353 lines
15 KiB
Dart
353 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
import 'package:fancake/core/data/backend.dart' show BackendException;
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/features/home/growth_complete_screen.dart';
|
|
import 'package:fancake/features/home/home_data.dart';
|
|
import 'package:fancake/features/home/home_screen.dart';
|
|
import 'package:fancake/features/quest/quest_data.dart';
|
|
import 'package:fancake/features/home/home_onboarding_screen.dart';
|
|
import 'package:fancake/features/home/nebula_create_screen.dart';
|
|
import 'package:fancake/features/home/planet_create_screen.dart';
|
|
|
|
/// 생성 호출을 기록하는 표본. [error] 가 있으면 서버 거부를 흉내 낸다.
|
|
class _CreateRepo extends SampleHomeRepository {
|
|
_CreateRepo({this.error, this.primaryChanged = true});
|
|
final BackendException? error;
|
|
final bool primaryChanged;
|
|
final calls = <String>[];
|
|
|
|
@override
|
|
Future<({String fandomId, bool primaryChanged})> createNebula(String name) async {
|
|
calls.add(name);
|
|
if (error case final e?) throw e;
|
|
return (fandomId: 'new-nebula', primaryChanged: primaryChanged);
|
|
}
|
|
|
|
@override
|
|
Future<void> createPlanet({
|
|
required String fandomId,
|
|
required String planetType,
|
|
required String colorKey,
|
|
}) async {
|
|
calls.add('$fandomId · $planetType · $colorKey');
|
|
if (error case final e?) throw e;
|
|
}
|
|
}
|
|
|
|
/// 운영값을 바꾼 `config/policy` 흉내.
|
|
class _PolicyRepo extends SampleHomeRepository {
|
|
@override
|
|
Future<NebulaTargets> nebulaTargets() async =>
|
|
nebulaTargetsFrom({'nebulaMinPop': 2000, 'nebulaMinMembers': 20, 'nebulaDays': 5});
|
|
}
|
|
|
|
/// 생성 화면을 위에 얹고, 닫힐 때 돌려준 값을 받는다.
|
|
Future<List<Object?>> _pushed(WidgetTester tester, Widget screen) async {
|
|
final results = <Object?>[];
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: Builder(
|
|
builder: (context) => TextButton(
|
|
onPressed: () async => results.add(
|
|
await Navigator.of(context).push<String>(
|
|
MaterialPageRoute(builder: (_) => screen),
|
|
),
|
|
),
|
|
child: const Text('열기'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.tap(find.text('열기'));
|
|
await tester.pumpAndSettle();
|
|
return results;
|
|
}
|
|
|
|
Future<void> _pump(WidgetTester tester, Widget screen) async {
|
|
await tester.pumpWidget(MaterialApp(theme: buildAppTheme(), home: screen));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
void main() {
|
|
group('성운 생성 (F1 · 340:75466)', () {
|
|
testWidgets('이름을 넣기 전에는 「성운 생성」이 없고 목표는 원장 고정값이다', (tester) async {
|
|
await _pump(tester, const NebulaCreateScreen());
|
|
expect(find.text('성운 생성'), findsNothing);
|
|
// 정책 원장 B-10 — 1,000 POP · 10명 · 7일. 사본의 슬라이더는 조작할 수 없다.
|
|
expect(find.text('1,000P / 10'), findsOneWidget);
|
|
expect(find.byType(Slider), findsNothing);
|
|
expect(find.textContaining('7일내'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('목표는 config/policy 운영값(서버와 같은 키)을 따른다', (tester) async {
|
|
await _pump(tester, NebulaCreateScreen(repository: _PolicyRepo()));
|
|
expect(find.text('2,000P / 20'), findsOneWidget);
|
|
expect(find.textContaining('5일내'), findsOneWidget);
|
|
});
|
|
|
|
test('운영값이 없거나 잘못되면 원장 기본값 1,000 · 10 · 7', () {
|
|
expect(nebulaTargetsFrom(null), defaultNebulaTargets);
|
|
expect(nebulaTargetsFrom({'nebulaMinPop': -1, 'nebulaMinMembers': 'x'}),
|
|
(pop: 1000, members: 10, days: 7));
|
|
});
|
|
|
|
testWidgets('서버가 쓰이는 이름(E-FDM-01)이라 하면 입력칸 오류로 알리고 만들 수 없다', (tester) async {
|
|
final repo = _CreateRepo(
|
|
error: const BackendException('E-FDM-01', '이미 사용중인 이름입니다.'),
|
|
);
|
|
await _pump(tester, NebulaCreateScreen(repository: repo));
|
|
await tester.enterText(find.byType(TextField), 'BTS');
|
|
await tester.pumpAndSettle();
|
|
// 중복 판정은 서버만 한다 — 입력만으로는 오류를 띄우지 않는다.
|
|
expect(find.text('이미 사용중인 이름입니다.'), findsNothing);
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('이미 사용중인 이름입니다.'), findsOneWidget);
|
|
final btn = tester.widget<FilledButton>(
|
|
find.widgetWithText(FilledButton, '성운 생성'),
|
|
);
|
|
expect(btn.onPressed, isNull);
|
|
});
|
|
|
|
testWidgets('이름을 넣고 만들면 기본 행성 변경을 되묻는다', (tester) async {
|
|
await _pump(tester, const NebulaCreateScreen());
|
|
await tester.enterText(find.byType(TextField), 'BTS_RM');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
// Figma 문구 그대로여야 한다.
|
|
expect(find.text('선택한 행성을 기본 행성으로 변경할까요?'), findsOneWidget);
|
|
expect(find.text('취소'), findsOneWidget);
|
|
expect(find.text('변경'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('변경을 누르면 fn-nebula-create 로 만들고 새 성운 ID 를 들고 닫힌다', (tester) async {
|
|
final repo = _CreateRepo();
|
|
final results = await _pushed(tester, NebulaCreateScreen(repository: repo));
|
|
await tester.enterText(find.byType(TextField), 'BTS_RM');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(repo.calls, ['BTS_RM']);
|
|
expect(results, ['new-nebula']);
|
|
expect(find.byType(NebulaCreateScreen), findsNothing);
|
|
});
|
|
|
|
testWidgets('서버가 거부하면 사유를 알리고 화면에 남는다', (tester) async {
|
|
final repo = _CreateRepo(
|
|
error: const BackendException('E-FDM-02', '이미 오늘 성운을 생성하셨습니다.'),
|
|
);
|
|
final results = await _pushed(tester, NebulaCreateScreen(repository: repo));
|
|
await tester.enterText(find.byType(TextField), 'BTS_RM');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('이미 오늘 성운을 생성하셨습니다.'), findsOneWidget);
|
|
expect(results, isEmpty);
|
|
expect(find.byType(NebulaCreateScreen), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('대표 변경 30일 대기 중이면 성운만 생겼다고 알리고 닫힌다(C-15)', (tester) async {
|
|
final repo = _CreateRepo(primaryChanged: false);
|
|
final results = await _pushed(tester, NebulaCreateScreen(repository: repo));
|
|
await tester.enterText(find.byType(TextField), 'BTS_RM');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('30일이 지나야'), findsOneWidget);
|
|
await tester.tap(find.text('확인'));
|
|
await tester.pumpAndSettle();
|
|
expect(results, ['new-nebula']);
|
|
});
|
|
|
|
testWidgets('보유 POP 은 여는 쪽이 넘긴 지갑 값이다', (tester) async {
|
|
await _pump(tester, const NebulaCreateScreen(myPop: 3210));
|
|
expect(find.textContaining('3,210'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('오늘 이미 만들었으면 사유를 알리고 진행하지 않는다', (tester) async {
|
|
await _pump(tester, const NebulaCreateScreen(alreadyCreatedToday: true));
|
|
await tester.enterText(find.byType(TextField), 'BTS_RM');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('성운 생성'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('이미 오늘 성운을 생성하셨습니다.'), findsOneWidget);
|
|
// 제한에 걸렸으면 변경 확인으로 넘어가면 안 된다.
|
|
expect(find.text('변경'), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('성장 완료 (F1 · 344:213778) — HOME 성운 상태', () {
|
|
testWidgets('두 조건을 모두 채우면 성운을 눌러 행성 생성으로 간다', (tester) async {
|
|
await _pump(tester, const GrowthCompleteScreen());
|
|
expect(find.text('성운을 클릭해서\n행성으로 탄생 시켜보세요!'), findsOneWidget);
|
|
await tester.tap(find.bySemanticsLabel('성운'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(PlanetCreateScreen), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('창립자가 아니면 조건을 채워도 탄생 안내 · 탭이 없다(Q-PN-01 A안)', (tester) async {
|
|
await _pump(tester, const GrowthCompleteScreen(isFounder: false));
|
|
expect(find.text('성운을 클릭해서\n행성으로 탄생 시켜보세요!'), findsNothing);
|
|
await tester.tap(find.bySemanticsLabel('성운'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.byType(PlanetCreateScreen), findsNothing);
|
|
});
|
|
|
|
testWidgets('한쪽만 채우면 대기다 — 정책 원장 B-10 동시 충족', (tester) async {
|
|
await _pump(
|
|
tester,
|
|
// 인원은 넘었으나 POP 이 모자란 경우.
|
|
const GrowthCompleteScreen(members: 12, contributedPop: 400),
|
|
);
|
|
expect(find.text('성운을 클릭해서\n행성으로 탄생 시켜보세요!'), findsNothing);
|
|
await tester.tap(find.bySemanticsLabel('성운'));
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
find.byType(PlanetCreateScreen),
|
|
findsNothing,
|
|
reason: '한쪽만 채웠는데 승격이 열리면 안 된다',
|
|
);
|
|
});
|
|
|
|
testWidgets('두 조건을 갈라 보여 준다', (tester) async {
|
|
await _pump(tester, const GrowthCompleteScreen());
|
|
expect(find.text('활동을 통해 얻은 POP을 통해\n성운에 기여해 보세요!'), findsOneWidget);
|
|
// 카드는 성장률 한 줄(사본 「성운 성장률 100%」) — 누르면 두 조건 상세.
|
|
await tester.tap(find.textContaining('성운 성장률'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('POP 조건 충족 · 1,240 / 1,000'), findsOneWidget);
|
|
expect(find.text('창립 멤버 조건 충족 · 12 / 10'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('행성 생성 (F1 · 344:214421)', () {
|
|
testWidgets('형태 → 색 두 단계만 고른다 — C-50', (tester) async {
|
|
final repo = _CreateRepo();
|
|
final results = await _pushed(
|
|
tester,
|
|
PlanetCreateScreen(fandomId: 'nebula-1', repository: repo),
|
|
);
|
|
expect(find.text('행성 타입 선택'), findsOneWidget);
|
|
await tester.tap(find.bySemanticsLabel('형태 2'));
|
|
await tester.tap(find.text('다음'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('행성 색 선택'), findsOneWidget);
|
|
expect(find.text('배경색 선택'), findsOneWidget);
|
|
// 생성 때 고르는 것은 형태와 컬러뿐 — 오브젝트 색 · 직접 고르기는 없다.
|
|
expect(find.text('오브젝트 색 선택'), findsNothing);
|
|
await tester.tap(find.bySemanticsLabel('gold'));
|
|
await tester.tap(find.text('완료').last);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('선택한 옵션으로\n행성을 생성할까요?'), findsOneWidget);
|
|
await tester.tap(find.text('생성'));
|
|
await tester.pumpAndSettle();
|
|
// fn-planet-create 에는 형태 · 컬러 두 값만 간다. 성공하면 닫혀 HOME 으로 돌아간다.
|
|
expect(repo.calls, ['nebula-1 · shape2 · gold']);
|
|
expect(results, ['nebula-1']);
|
|
expect(find.byType(PlanetCreateScreen), findsNothing);
|
|
});
|
|
|
|
testWidgets('전환 조건 미달 등 서버 거부는 사유를 띄우고 화면에 남는다', (tester) async {
|
|
final repo = _CreateRepo(
|
|
error: const BackendException('E-FDM-03', '성운 전환 조건을 아직 채우지 못했습니다.'),
|
|
);
|
|
await _pump(tester, PlanetCreateScreen(fandomId: 'n', repository: repo));
|
|
await tester.tap(find.text('다음'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('완료').last);
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('생성'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('성운 전환 조건을 아직 채우지 못했습니다.'), findsOneWidget);
|
|
expect(find.byType(PlanetCreateScreen), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('색 단계에서 이전으로 돌아간다', (tester) async {
|
|
await _pump(tester, const PlanetCreateScreen());
|
|
await tester.tap(find.text('다음'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('이전'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('행성 타입 선택'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('초기 사용자 홈 (F1 · 343:212939) — HOME 무소속 상태', () {
|
|
testWidgets('「+」 를 누르면 행성 생성·가입 시트가 뜨고 고른 동작을 부른다', (tester) async {
|
|
var created = 0;
|
|
await _pump(
|
|
tester,
|
|
HomeOnboardingScreen(onCreateNebula: () => created++),
|
|
);
|
|
// 사용자 노출 브랜드는 팬우주다(공통 기준 4항).
|
|
expect(find.text('행성을 생성 또는 가입하고\n팬우주를 시작해 보세요!'), findsOneWidget);
|
|
await tester.tap(find.bySemanticsLabel('행성 생성 또는 가입'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('행성 가입'), findsOneWidget);
|
|
expect(find.text('취소'), findsOneWidget);
|
|
await tester.tap(find.text('행성 생성 (성운)'));
|
|
await tester.pumpAndSettle();
|
|
expect(created, 1);
|
|
expect(find.text('행성 가입'), findsNothing);
|
|
});
|
|
});
|
|
|
|
testWidgets('헤더 지갑은 「아이템 구입」 시트를 열고 구매는 준비 중이다', (tester) async {
|
|
tester.view.physicalSize = const Size(1080, 2400);
|
|
tester.view.devicePixelRatio = 2;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: const HomeScreen(
|
|
repository: SampleHomeRepository(),
|
|
questRepository: SampleQuestRepository(),
|
|
eclipse: NoEclipseRepository(),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(seconds: 1));
|
|
await tester.tap(find.byTooltip('아이템'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 400));
|
|
expect(find.text('아이템 구입'), findsOneWidget);
|
|
expect(find.text('₩1,200'), findsOneWidget);
|
|
await tester.tap(find.text('베이직 드랍'));
|
|
await tester.pump();
|
|
expect(find.text('아이템 구입은 준비 중입니다.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('F1 화면 문구에 내부 경로가 새지 않는다', (tester) async {
|
|
for (final screen in <Widget>[
|
|
const NebulaCreateScreen(),
|
|
const GrowthCompleteScreen(),
|
|
const PlanetCreateScreen(),
|
|
const HomeOnboardingScreen(),
|
|
]) {
|
|
await _pump(tester, screen);
|
|
final texts = tester
|
|
.widgetList<Text>(find.byType(Text))
|
|
.map((w) => w.data ?? '')
|
|
.join('\n');
|
|
for (final leak in ['lib/', '.dart', 'Exception', 'wireframe/']) {
|
|
expect(texts.contains(leak), isFalse, reason: '「$leak」 노출');
|
|
}
|
|
}
|
|
});
|
|
}
|