generated from ttipo/project-base
test(flutter): 서버 모드 화면 흐름 테스트
기존 흐름 테스트는 기기 저장 모드로 돈다. 가짜 서버를 붙여 서버 모드에서도 로그인 → 온보딩 → 홈(서버의 아이) 흐름, 세션 만료 시 로그인 화면 복귀(AUTH_002), 로그인 전에는 서버에 묻지 않는 것을 화면 단위로 확인한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tma_app/app.dart';
|
||||
import 'package:tma_app/core/api/dio_provider.dart';
|
||||
import 'package:tma_app/core/api/session_store.dart';
|
||||
import 'package:tma_app/core/config/app_config.dart';
|
||||
import 'package:tma_app/core/storage/preferences_provider.dart';
|
||||
import 'package:tma_app/features/auth/data/repositories/device_key_store.dart';
|
||||
import 'package:tma_app/features/auth/presentation/pages/login_page.dart';
|
||||
import 'package:tma_app/features/onboarding/presentation/pages/onboarding_page.dart';
|
||||
import 'package:tma_app/features/shell/presentation/widgets/rup_tab_bar.dart';
|
||||
|
||||
import 'support/app_harness.dart' show settle;
|
||||
import 'support/fake_api.dart';
|
||||
|
||||
/// 서버에 이미 등록된 아이
|
||||
Map<String, Object?> _kongJson() => {
|
||||
'id': 'pet-kong',
|
||||
'tint': '#FF9B6A',
|
||||
'name': '콩이',
|
||||
'species': '강아지',
|
||||
'breed': '말티즈',
|
||||
'gender': 'male',
|
||||
'neutered': true,
|
||||
'birthDate': '2021-09-14',
|
||||
'weight': 3.2,
|
||||
'weightUnit': 'kg',
|
||||
'photoFileId': null,
|
||||
'microchip': null,
|
||||
'bloodType': null,
|
||||
'vet': null,
|
||||
'memo': null,
|
||||
'allergies': <String>[],
|
||||
'conditions': <String>[],
|
||||
'watchConditions': <String>[],
|
||||
'archive': null,
|
||||
};
|
||||
|
||||
/// 서버 하나를 흉내 낸다 — 로그인·내 정보·목록들
|
||||
void _serve(FakeApi api, {required bool sessionAlive}) {
|
||||
api.on('POST', '/auth/dev-sessions', (r) => ok({
|
||||
'accessToken': 'access-1',
|
||||
'refreshToken': 'refresh-1',
|
||||
'accessTokenExpiresIn': 3600,
|
||||
'refreshTokenExpiresAt': '2026-10-25T00:00:00.000Z',
|
||||
'user': {
|
||||
'id': 'user-1',
|
||||
'nickname': '콩이 보호자',
|
||||
'photoFileId': null,
|
||||
'provider': 'kakao',
|
||||
'createdAt': '2026-10-11T00:00:00.000Z',
|
||||
},
|
||||
'isNewUser': true,
|
||||
}, status: 201));
|
||||
api.on('POST', '/auth/tokens', (r) => failure('AUTH_001', 401));
|
||||
|
||||
FakeReply guarded(Object? data) => sessionAlive ? ok(data) : failure('AUTH_001', 401);
|
||||
api.on('GET', '/users/me', (r) => guarded({
|
||||
'id': 'user-1',
|
||||
'nickname': '콩이 보호자',
|
||||
'photoFileId': null,
|
||||
'provider': 'kakao',
|
||||
'createdAt': '2026-10-11T00:00:00.000Z',
|
||||
}));
|
||||
api.on('GET', '/pets', (r) => guarded(page([_kongJson()])));
|
||||
for (final path in [
|
||||
'/schedule-events',
|
||||
'/repeat-rules',
|
||||
'/repeat-rule-completions',
|
||||
'/diaries',
|
||||
'/diary-posts',
|
||||
'/diary-comments',
|
||||
'/pet-cards',
|
||||
]) {
|
||||
api.on('GET', path, (r) => guarded(page(const [])));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pumpApiApp(WidgetTester tester, FakeApi api, Map<String, Object> saved) async {
|
||||
tester.view.physicalSize = const Size(402, 874);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
SharedPreferences.setMockInitialValues(saved);
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||
dataModeProvider.overrideWithValue(DataMode.api),
|
||||
sessionStoreProvider.overrideWithValue(api.session),
|
||||
dioProvider.overrideWithValue(api.dio),
|
||||
deviceKeyStoreProvider.overrideWithValue(const FixedDeviceKeyStore('test-device-key-000001')),
|
||||
],
|
||||
child: const RupApp(),
|
||||
),
|
||||
);
|
||||
// 스플래시 이미지 디코딩 → 2.6초 뒤 다음 화면
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 300)));
|
||||
await tester.pump(const Duration(seconds: 4));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
/// 가짜 서버 응답(비동기)이 화면에 반영되고, 그 화면이 부른 요청까지 모두 끝날 때까지
|
||||
Future<void> _network(WidgetTester tester, FakeApi api) async {
|
||||
var seen = -1;
|
||||
for (var i = 0; i < 10 && seen != api.requests.length; i++) {
|
||||
seen = api.requests.length;
|
||||
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
||||
await settle(tester);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('서버 모드: 로그인하면 토큰을 받고 온보딩을 거쳐 홈에서 서버의 아이를 보여 준다', (tester) async {
|
||||
final api = FakeApi(tokens: null);
|
||||
_serve(api, sessionAlive: true);
|
||||
await _pumpApiApp(tester, api, {});
|
||||
|
||||
await tester.tap(find.text('시작하기'));
|
||||
await settle(tester);
|
||||
expect(find.byType(LoginPage), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('카카오로 3초만에 시작하기'));
|
||||
await _network(tester, api);
|
||||
expect(api.session.tokens?.accessToken, 'access-1');
|
||||
expect(find.byType(OnboardingPage), findsOneWidget);
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await tester.tap(find.byType(OnboardingPage));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
await tester.tap(find.text('시작하기'));
|
||||
await _network(tester, api);
|
||||
|
||||
expect(find.byType(RupTabBar), findsOneWidget);
|
||||
expect(find.text('콩이'), findsWidgets);
|
||||
expect(api.sent('GET', '/pets'), isNotEmpty);
|
||||
expect(api.reported, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('서버가 세션을 끊으면(갱신 거절) 로그인 화면으로 돌아가고 세션 만료로 알린다', (tester) async {
|
||||
final api = FakeApi(tokens: const AuthTokens(accessToken: 'stale', refreshToken: 'revoked'));
|
||||
_serve(api, sessionAlive: false);
|
||||
await _pumpApiApp(tester, api, {'entry.onboardingSeen': true});
|
||||
await _network(tester, api);
|
||||
|
||||
expect(api.session.hasSession, isFalse);
|
||||
expect(api.session.expired, isTrue);
|
||||
expect(find.byType(LoginPage), findsOneWidget);
|
||||
expect(api.reported.map((e) => e.code).toSet(), {'AUTH_002'});
|
||||
});
|
||||
|
||||
testWidgets('로그인 전(세션 없음)에는 서버에 기록을 묻지 않는다', (tester) async {
|
||||
final api = FakeApi(tokens: null);
|
||||
_serve(api, sessionAlive: true);
|
||||
await _pumpApiApp(tester, api, {});
|
||||
|
||||
expect(api.requests, isEmpty);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user