510 lines
23 KiB
Dart
510 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:fancake/core/theme/app_theme.dart';
|
|
import 'package:fancake/features/eclipse/eclipse_data.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_data.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_motion.dart';
|
|
import 'package:fancake/core/data/backend.dart' show BackendException;
|
|
import 'package:fancake/features/galaxy/galaxy_home_screen.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_screen.dart';
|
|
import 'package:fancake/features/galaxy/galaxy_search_screen.dart';
|
|
import 'package:fancake/features/galaxy/widgets/zap_button.dart';
|
|
import 'package:fancake/features/planet/planet_data.dart';
|
|
import 'package:fancake/features/planet/planet_detail_screen.dart';
|
|
|
|
import 'eclipse_stubs.dart';
|
|
|
|
/// 저장소를 전부 주입한 궤도 맵. 화면이 표본을 기본값으로 만들지 않으므로
|
|
/// (공통 규칙 — 저장소 선택은 `repositories.dart` 한 곳) 테스트가 직접 넘긴다.
|
|
GalaxyScreen _galaxy({
|
|
GalaxyRepository repository = const SampleGalaxyRepository(),
|
|
EclipseRepository eclipse = const NoEclipseRepository(),
|
|
}) => GalaxyScreen(
|
|
repository: repository,
|
|
planetRepository: samplePlanetRepository,
|
|
eclipse: eclipse,
|
|
);
|
|
|
|
/// 갤럭시 맵은 궤도 회전 애니메이션이 끊이지 않는다. `pumpAndSettle` 은 영원히
|
|
/// 기다리므로 이 화면 계열에서는 시간을 정해 [WidgetTester.pump] 한다.
|
|
Future<void> _pump(WidgetTester tester, Widget screen) async {
|
|
await tester.pumpWidget(MaterialApp(theme: buildAppTheme(), home: screen));
|
|
await tester.pump(); // FutureBuilder
|
|
await tester.pump(const Duration(milliseconds: 950)); // 이벤트 띠 슬라이드
|
|
}
|
|
|
|
/// 두 손가락으로 끝까지 확대하며 오른쪽 아래로 끈다(한 손가락 팬은 없다).
|
|
Future<void> _pinch(WidgetTester tester) async {
|
|
final origin = tester.getTopLeft(find.byType(InteractiveViewer));
|
|
final a = await tester.startGesture(origin + const Offset(4, 4));
|
|
final b = await tester.startGesture(
|
|
origin + const Offset(24, 24),
|
|
pointer: 7,
|
|
);
|
|
for (var i = 1; i <= 10; i++) {
|
|
await b.moveTo(origin + Offset(24.0 + i * 32, 24.0 + i * 32));
|
|
await a.moveTo(origin + Offset(4.0 + i * 20, 4.0 + i * 20));
|
|
await tester.pump();
|
|
}
|
|
await a.up();
|
|
await b.up();
|
|
await _settle(tester);
|
|
}
|
|
|
|
/// 회전이 도는 동안에도 안전하게 몇 프레임 넘긴다.
|
|
Future<void> _settle(WidgetTester tester) async {
|
|
for (var i = 0; i < 6; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
}
|
|
|
|
/// 궤도 꼭대기에 바깥 궤도 행성을 세우는 표본. 목록 index 가 궤도 각도를 정하므로
|
|
/// (`index * 2pi / 5.4`) index 4 가 화면 위쪽 끝이다.
|
|
class _TopOrbitRepository implements GalaxyRepository {
|
|
const _TopOrbitRepository();
|
|
|
|
@override
|
|
Future<void> setPrimary(String fandomId) async {}
|
|
|
|
@override
|
|
Future<List<PlanetNode>> search(String query) async => const [];
|
|
|
|
@override
|
|
Future<ZapResult> zap({
|
|
required String fandomId,
|
|
required int count,
|
|
required String clientSeq,
|
|
}) async => const ZapResult(txId: '', balanceAfter: -1);
|
|
|
|
@override
|
|
Future<GalaxySnapshot> load() async => const GalaxySnapshot(
|
|
myPop: 0,
|
|
joined: false,
|
|
planets: [
|
|
PlanetNode(id: 'mine', name: '내 행성', rank: 1, fpi: 100, members: 1, online: 1, isMine: true),
|
|
PlanetNode(id: 'a', name: 'A', rank: 2, fpi: 90, members: 1, online: 1),
|
|
PlanetNode(id: 'b', name: 'B', rank: 3, fpi: 80, members: 1, online: 1),
|
|
PlanetNode(id: 'c', name: 'C', rank: 4, fpi: 70, members: 1, online: 1),
|
|
PlanetNode(id: 'top', name: 'TOP', rank: 6, fpi: 60, members: 1, online: 1),
|
|
PlanetNode(id: 'e', name: 'E', rank: 5, fpi: 50, members: 1, online: 1),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 내 행성이 1위가 아닌 표본 — Ranking View 전환이 눈에 보이게 한다.
|
|
class _RankedRepository extends _TopOrbitRepository {
|
|
const _RankedRepository();
|
|
|
|
@override
|
|
Future<GalaxySnapshot> load() async => const GalaxySnapshot(
|
|
myPop: 0,
|
|
joined: false,
|
|
planets: [
|
|
PlanetNode(id: 'top', name: 'TOP', rank: 1, fpi: 900, members: 99, online: 1, stage: 4),
|
|
PlanetNode(id: 'a', name: 'A', rank: 2, fpi: 500, members: 5, online: 1),
|
|
PlanetNode(id: 'mine', name: '내 행성', rank: 3, fpi: 100, members: 7, online: 1, isMine: true),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 대표 팬덤 변경 호출을 기록하는 표본. [error] 가 있으면 서버 거부를 흉내 낸다.
|
|
class _PrimaryRepository extends SampleGalaxyRepository {
|
|
_PrimaryRepository({this.error});
|
|
final BackendException? error;
|
|
final changes = <String>[];
|
|
|
|
@override
|
|
Future<void> setPrimary(String fandomId) async {
|
|
if (error case final e?) throw e;
|
|
changes.add(fandomId);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
group('GALAXY 모아보기 (§2.56 · §2.58) — 기본 행성 변경', () {
|
|
Future<void> pumpHome(WidgetTester tester, GalaxyRepository repo) async {
|
|
await tester.pumpWidget(MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: GalaxyHomeScreen(
|
|
repository: repo,
|
|
planetRepository: samplePlanetRepository,
|
|
eclipse: const NoEclipseRepository(),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('대표가 아닌 가입 행성을 누르면 변경 모달 → fn-primary-fandom-set', (tester) async {
|
|
final repo = _PrimaryRepository();
|
|
await pumpHome(tester, repo);
|
|
await tester.tap(find.bySemanticsLabel('NewJeans'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('선택한 행성을 기본 행성으로 변경할까요?'), findsOneWidget);
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(repo.changes, ['newjeans']);
|
|
});
|
|
|
|
testWidgets('30일 대기 중이면 서버 사유(E-FDM-04)를 알린다', (tester) async {
|
|
final repo = _PrimaryRepository(
|
|
error: const BackendException('E-FDM-04', '기본 행성은 변경 후 30일이 지나야 다시 바꿀 수 있습니다.'),
|
|
);
|
|
await pumpHome(tester, repo);
|
|
await tester.tap(find.bySemanticsLabel('NewJeans'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.text('변경'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('30일이 지나야'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('갤럭시 맵 (F2 · 354:220228)', () {
|
|
testWidgets('내 행성 HUD 가 Rank·FPI·접속자수를 띄운다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
// 명세 — HUD 데이터 : Rank, FPI 총점, 접속자수(맴버수)
|
|
expect(find.text('RANK'), findsOneWidget);
|
|
expect(find.text('FPI'), findsOneWidget);
|
|
expect(find.textContaining('접속 1,204명'), findsOneWidget);
|
|
expect(find.textContaining('멤버 12,840명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Ranking View 를 켜면 FPI 1위로 중심이 옮겨 가고 끄면 내 행성으로 돌아온다', (tester) async {
|
|
await _pump(tester, _galaxy(repository: const _RankedRepository()));
|
|
// 진입은 내 Primary 행성 중심(B-21) — HUD 가 내 행성을 가리킨다.
|
|
expect(find.textContaining('멤버 7명'), findsOneWidget);
|
|
await tester.tap(find.byTooltip('Ranking View'));
|
|
await _settle(tester);
|
|
expect(find.textContaining('멤버 99명'), findsOneWidget);
|
|
expect(find.textContaining('멤버 7명'), findsNothing);
|
|
await tester.tap(find.byTooltip('Ranking View'));
|
|
await _settle(tester);
|
|
expect(find.textContaining('멤버 7명'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('줌인·줌아웃과 터치 이동이 가능하다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
// 명세 — 3d갤럭시 : 브라우징 줌인 줌아웃 / 터치 이동
|
|
expect(find.byType(InteractiveViewer), findsOneWidget);
|
|
final v = tester.widget<InteractiveViewer>(find.byType(InteractiveViewer));
|
|
expect(v.minScale, lessThan(1));
|
|
expect(v.maxScale, greaterThan(1));
|
|
});
|
|
|
|
// 검토회신서 3절 — *「선택 행성 정보가 행성과 연계된 HUD 가 아니라 화면
|
|
// 최상단 고정 패널 형태」*. 고정 패널로 되돌아가면 여기서 깨진다.
|
|
testWidgets('HUD 는 화면이 아니라 행성에 붙어 움직인다', (tester) async {
|
|
tester.view.physicalSize = const Size(1200, 2000);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
await _pump(tester, _galaxy());
|
|
await tester.tap(find.byKey(const ValueKey('newjeans')));
|
|
await _settle(tester);
|
|
final before = tester.getCenter(find.text('RANK'));
|
|
// 확대하면 행성이 커지고, 행성 위에 붙은 카드는 행성 가장자리를 따라 밀려난다.
|
|
await _pinch(tester);
|
|
final after = tester.getCenter(find.text('RANK'));
|
|
expect(
|
|
(after - before).distance,
|
|
greaterThan(20),
|
|
reason: '화면 고정 패널이면 확대해도 제자리에 있다',
|
|
);
|
|
});
|
|
|
|
// 검토회신서 3절 — *「행성 선택 시 중심 전환과 HUD 데이터의 즉시 연동」*.
|
|
testWidgets('행성을 고르면 HUD 가 즉시 바뀌고 중심 전환은 감속하며 간다', (tester) async {
|
|
// 기본 테스트 화면(800×600)에서는 궤도가 하단 이벤트 띠와 겹쳐 탭이
|
|
// 띠에 먹힌다. 세로를 넉넉히 잡아 겹침을 없앤다.
|
|
tester.view.physicalSize = const Size(1200, 2000);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
await _pump(tester, _galaxy());
|
|
await tester.tap(find.byKey(const ValueKey('newjeans')));
|
|
await tester.pump();
|
|
// 즉시 연동 — 고른 행성의 FPI 총점이 바로 HUD 에 올라온다.
|
|
expect(find.text('271,905'), findsOneWidget);
|
|
|
|
// HUD 카드는 행성 위에 가운데를 맞춰 선다. 게이지의 x 가 곧 행성의 x 다.
|
|
final gauge = find.byType(LinearProgressIndicator);
|
|
final mid = tester.getSize(find.byType(Scaffold)).width / 2;
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final early = (tester.getCenter(gauge).dx - mid).abs();
|
|
await tester.pump(GalaxyMotion.recenter);
|
|
final settled = (tester.getCenter(gauge).dx - mid).abs();
|
|
|
|
expect(settled, lessThan(12), reason: '전환이 끝나면 고른 행성이 한가운데다');
|
|
expect(early, greaterThan(settled + 20),
|
|
reason: '한 프레임에 순간이동하면 「행성 간 이동감」이 없다');
|
|
});
|
|
|
|
// 리뷰 M3 — HUD 위/아래 뒤집힘이 비행 중에 일어나면 카드가 행성을 가로질러
|
|
// 순간 이동한다. 선택 시점에 정한 방향을 전환이 끝날 때까지 붙들어야 한다.
|
|
//
|
|
// 표본 6행성은 어느 것도 화면 위쪽 끝에서 출발하지 않아 뒤집힘이 나지 않는다.
|
|
// 궤도 꼭대기(목록 index 4 → 각도 ≈ -pi/2)에 바깥 궤도 행성을 놓아 재현한다.
|
|
testWidgets('중심 전환 중에 HUD 가 행성 위아래로 뒤집히지 않는다', (tester) async {
|
|
// 세로 800 이 분기점이다 — 출발점에는 카드가 들어갈 자리가 없고(아래),
|
|
// 도착점인 화면 한가운데에는 있다(위).
|
|
tester.view.physicalSize = const Size(800, 800);
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
await _pump(tester, _galaxy(repository: const _TopOrbitRepository()));
|
|
await tester.tap(find.byKey(const ValueKey('top')));
|
|
await tester.pump();
|
|
expect(find.byKey(const ValueKey('hud-top')), findsOneWidget);
|
|
|
|
final planet = find.byKey(const ValueKey('top'));
|
|
final card = find.byType(LinearProgressIndicator);
|
|
int? side;
|
|
// 전환이 끝나기 전까지(620ms) 방향이 한 번도 바뀌면 안 된다.
|
|
for (var i = 0; i < 30; i++) {
|
|
await tester.pump(const Duration(milliseconds: 16));
|
|
final now =
|
|
tester.getCenter(card).dy < tester.getCenter(planet).dy ? -1 : 1;
|
|
side ??= now;
|
|
expect(now, side, reason: '비행 ${i * 16}ms 에서 HUD 가 반대편으로 넘어갔다');
|
|
}
|
|
});
|
|
|
|
// 명세 — *HUD 터치시 FPI 상세 페이지 이동*. 목 모드에서 저장소가 누른 행성
|
|
// ID 를 버리면 여섯 행성이 전부 내 행성(BTS) 상세로 열린다.
|
|
testWidgets('남의 행성 HUD 를 누르면 그 행성 상세가 열린다', (tester) async {
|
|
tester.view.physicalSize = const Size(1200, 2400);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
|
|
// 기본 팩토리(samplePlanetRepository)를 그대로 쓴다 — 실제 앱 경로와 같다.
|
|
await _pump(tester, _galaxy());
|
|
await tester.tap(find.byKey(const ValueKey('newjeans')));
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.byKey(const ValueKey('hud-newjeans')));
|
|
// 갤럭시는 회전이 멈추지 않으므로 pumpAndSettle 을 쓸 수 없다.
|
|
for (var i = 0; i < 20; i++) {
|
|
await tester.pump(const Duration(milliseconds: 60));
|
|
}
|
|
|
|
expect(find.textContaining('TOTAL FPI'), findsOneWidget, reason: '상세로 넘어가지 않았다');
|
|
expect(find.text('NewJeans'), findsWidgets);
|
|
expect(find.textContaining('271,905'), findsWidgets);
|
|
// 내 행성 상세가 열리면 이 값들이 보인다. 하나라도 보이면 ID 를 버린 것이다.
|
|
expect(find.text('BTS'), findsNothing, reason: '내 행성 상세가 열렸다');
|
|
expect(find.textContaining('284,310'), findsNothing, reason: '내 행성 상세가 열렸다');
|
|
// 남의 행성에는 내 기여도가 없다 — 대조표 3-5.
|
|
expect(find.textContaining(RegExp(r'상위 \d+%')), findsNothing);
|
|
});
|
|
|
|
// 리뷰 M2 — 행성이 뷰포트 밖이면 HUD 를 아예 그리지 않는다. 카드만 가장자리에
|
|
// clamp 해 두면 어느 행성을 가리키는지 알 수 없고 연결선은 화면 밖에 그려진다.
|
|
// 고른 행성은 카메라가 따라가므로(10/5) 확대·초점 이동으로도 화면 밖으로 밀리지 않는다.
|
|
testWidgets('확대해도 고른 행성은 가운데에 남고 HUD 도 남는다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
final mid = tester.getCenter(find.byType(InteractiveViewer));
|
|
await _pinch(tester);
|
|
expect(find.text('RANK'), findsOneWidget);
|
|
final mine = tester.widget<InteractiveViewer>(find.byType(InteractiveViewer));
|
|
expect(mine.transformationController!.value.getMaxScaleOnAxis(),
|
|
greaterThan(GalaxyMotion.focusZoom));
|
|
final at = tester.getCenter(find.text('RANK'));
|
|
expect((at.dx - mid.dx).abs(), lessThan(mid.dx), reason: '카드가 화면 안');
|
|
});
|
|
|
|
// 10/5 실기기(Flip3) — 다른 행성을 탭하면 중심 전환 뒤에도 궤도가 돌아 고른 행성이
|
|
// 화면 밖으로 흘러가고 내 행성은 가장자리에 남았다. 명세 「행성 터치시 화면 중심 전환」.
|
|
testWidgets('고른 행성은 공전·끌기 회전 중에도 화면 가운데에 머문다', (tester) async {
|
|
tester.view.physicalSize = const Size(1200, 2000);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
await _pump(tester, _galaxy());
|
|
final viewer = find.byType(InteractiveViewer);
|
|
final mid = tester.getCenter(viewer);
|
|
final planet = find.byKey(const ValueKey('newjeans'));
|
|
|
|
await tester.tap(planet);
|
|
for (var i = 0; i < 25; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120)); // 3초 공전
|
|
}
|
|
expect((tester.getCenter(planet) - mid).distance, lessThan(8));
|
|
|
|
await tester.fling(viewer, const Offset(-300, 0), 1500);
|
|
for (var i = 0; i < 25; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
expect(
|
|
(tester.getCenter(planet) - mid).distance,
|
|
lessThan(8),
|
|
reason: '관성 회전 뒤에도 고른 행성이 가운데',
|
|
);
|
|
});
|
|
|
|
// 10/5 실기기 — HUD 카드 「>」를 눌러도 행성 상세로 가지 않았다.
|
|
// 명세 「HUD 터치시 FPI 상세 페이지 이동」.
|
|
testWidgets('HUD 카드의 화살표를 누르면 고른 행성의 상세로 간다', (tester) async {
|
|
tester.view.physicalSize = const Size(1200, 2000);
|
|
tester.view.devicePixelRatio = 2.0;
|
|
addTearDown(tester.view.reset);
|
|
await _pump(tester, _galaxy());
|
|
await tester.tap(find.byKey(const ValueKey('newjeans')));
|
|
for (var i = 0; i < 10; i++) {
|
|
await tester.pump(const Duration(milliseconds: 120));
|
|
}
|
|
await tester.tap(find.byIcon(Icons.chevron_right));
|
|
await _settle(tester);
|
|
expect(find.byType(PlanetDetailScreen), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('가로로 끌면 장면은 제자리에서 궤도가 돌고, 손을 떼도 관성으로 더 돈다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
final viewer = tester.widget<InteractiveViewer>(
|
|
find.byType(InteractiveViewer),
|
|
);
|
|
final view = viewer.transformationController!;
|
|
final before = view.value.clone();
|
|
final mine = tester.getCenter(find.text('RANK'));
|
|
|
|
for (var i = 0; i < 4; i++) {
|
|
await tester.fling(
|
|
find.byType(InteractiveViewer),
|
|
const Offset(-300, 0),
|
|
1500,
|
|
);
|
|
await tester.pump();
|
|
}
|
|
expect(view.value, before, reason: '끌기는 맵을 옮기지 않는다(팬 없음)');
|
|
expect(
|
|
(tester.getCenter(find.text('RANK')) - mine).distance,
|
|
lessThan(1),
|
|
reason: '가운데 내 행성은 화면 밖으로 밀려나지 않는다',
|
|
);
|
|
|
|
// 관성 — 손을 뗀 뒤에도 기본 표류보다 빠르게 돌다가 감속한다.
|
|
final state = tester.state(find.byType(GalaxyScreen));
|
|
final clock = (state as dynamic).clockForTest as GalaxyClock;
|
|
final v0 = clock.velocity;
|
|
expect(v0.abs(), greaterThan(GalaxyMotion.idleSpin));
|
|
await _settle(tester);
|
|
expect(
|
|
(clock.velocity - GalaxyMotion.idleSpin).abs(),
|
|
lessThan((v0 - GalaxyMotion.idleSpin).abs()),
|
|
);
|
|
});
|
|
|
|
testWidgets('GALAXY 는 좌측 하단, 돋보기는 우측 하단', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
expect(find.text('GALAXY'), findsOneWidget);
|
|
final label = tester.getCenter(find.text('GALAXY'));
|
|
final glass = tester.getCenter(find.byIcon(Icons.search));
|
|
final size = tester.getSize(find.byType(Scaffold));
|
|
expect(label.dx, lessThan(size.width / 2), reason: 'GALAXY 는 좌측');
|
|
expect(glass.dx, greaterThan(size.width / 2), reason: '돋보기는 우측');
|
|
});
|
|
|
|
testWidgets('돋보기를 누르면 검색 화면으로 간다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
await tester.tap(find.byIcon(Icons.search));
|
|
await _settle(tester);
|
|
expect(find.byType(GalaxySearchScreen), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('Zap (F2 · 명세 「Zap 버튼」)', () {
|
|
testWidgets('가입한 사용자에게는 노출된다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
expect(find.byType(ZapButton), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('가입·생성하지 않은 사용자에게는 노출되지 않는다', (tester) async {
|
|
await _pump(
|
|
tester,
|
|
_galaxy(repository: const SampleGalaxyRepository(joined: false)),
|
|
);
|
|
expect(find.byType(ZapButton), findsNothing);
|
|
});
|
|
|
|
testWidgets('롱프레스로 옵션이 열린다', (tester) async {
|
|
await _pump(tester, _galaxy());
|
|
// 명세 — 2초간 롱프레싱할 경우 Zap 옵션 버튼으로 전환
|
|
final hold = await tester.startGesture(tester.getCenter(find.byType(ZapButton)));
|
|
await tester.pump(ZapButton.longPress);
|
|
await hold.up();
|
|
await _settle(tester);
|
|
for (final label in ['기여', '아이템', '닫기']) {
|
|
expect(find.text(label), findsOneWidget, reason: '옵션 「$label」 누락');
|
|
}
|
|
// 「자동(오토 도네이션)」은 두지 않는다 — 별첨 1 ① 시나리오 4 가 적었으나
|
|
// 별첨 B 가 상위 기준이고 거기에 없는 기능이다(공통 기준 2항).
|
|
expect(find.text('자동'), findsNothing);
|
|
});
|
|
|
|
testWidgets('POP 이 모자라면 누를 수 없다', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: buildAppTheme(),
|
|
home: Scaffold(
|
|
body: ZapButton(zapPop: 10, myPop: 3, onZap: () {}),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('POP이 모자랍니다'), findsOneWidget);
|
|
final btn = tester.widget<FilledButton>(find.byType(FilledButton));
|
|
expect(btn.onPressed, isNull);
|
|
});
|
|
});
|
|
|
|
group('갤럭시 검색 (F2 · 353:217832)', () {
|
|
testWidgets('처음에는 최근 검색어가 없다고 알린다', (tester) async {
|
|
await _pump(tester, const GalaxySearchScreen());
|
|
expect(find.text('최근 검색어'), findsOneWidget);
|
|
expect(find.text('최근 검색어가 없습니다.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('검색하면 팬덤이 걸리고 최근 검색어에 남는다', (tester) async {
|
|
await _pump(tester, const GalaxySearchScreen());
|
|
await tester.enterText(find.byType(TextField), 'BTS');
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('멤버 12,840명'), findsOneWidget);
|
|
|
|
await tester.testTextInput.receiveAction(TextInputAction.search);
|
|
await tester.pumpAndSettle();
|
|
await tester.enterText(find.byType(TextField), '');
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('전체 삭제'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('결과 줄을 누르면 그 행성 상세로 간다(10/5 실기기)', (tester) async {
|
|
PlanetNode? opened;
|
|
await _pump(tester, GalaxySearchScreen(onOpen: (p) => opened = p));
|
|
await tester.enterText(find.byType(TextField), 'BTS');
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.textContaining('멤버 12,840명'));
|
|
expect(opened?.name, 'BTS');
|
|
});
|
|
|
|
testWidgets('없는 이름이면 사유를 알린다', (tester) async {
|
|
await _pump(tester, const GalaxySearchScreen());
|
|
await tester.enterText(find.byType(TextField), 'zzzz');
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('찾는 팬덤이 없습니다'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
testWidgets('F2 화면 문구에 내부 경로가 새지 않는다', (tester) async {
|
|
for (final screen in <Widget>[
|
|
_galaxy(),
|
|
const GalaxySearchScreen(),
|
|
]) {
|
|
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」 노출');
|
|
}
|
|
}
|
|
});
|
|
}
|