Files
fanuzu/app/test/design_match/a_test.dart
T

276 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@Tags(['design'])
library;
import 'dart:convert';
import 'dart:io';
import 'dart:ui' as ui;
import 'package:fancake/core/data/repositories.dart';
import 'package:fancake/core/router/app_router.dart';
import 'package:fancake/core/theme/app_theme.dart';
import 'package:fancake/features/auth/auth_repository.dart';
import 'package:fancake/features/eclipse/eclipse_data.dart';
import 'package:fancake/features/eclipse/eclipse_defend_screen.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';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../eclipse_stubs.dart';
import 'home_fixtures.dart';
/// 디자인 일치 그룹 A(F1 홈 · F7 이클립스) 대조 렌더.
///
/// 공유 하네스(`design_match_test.dart`)와 같은 방식으로 찍되, 표본을 Figma 사본의
/// 기본 상태에 맞춘다 — 홈은 라이브 띠·이클립스 경보가 없는 「ZAP 닫힘」 상태다.
/// 실행: `DESIGN_MATCH_OUT=<폴더> fvm flutter test --tags design test/design_match/a_test.dart`
const _width = 375.0;
const _padding = FakeViewPadding(top: 48 * 2, bottom: 34 * 2);
typedef Build = Future<Widget> Function(Repositories r);
class Shot {
const Shot(
this.id, {
this.build,
this.tab,
this.h = 812,
this.after,
this.repos = QuietRepos.new,
});
final String id;
final QuietRepos Function() repos;
final Build? build;
final String? tab;
final double h;
final Future<void> Function(WidgetTester t)? after;
}
/// 이클립스 화면 — 사본 문구의 「60초」가 나오도록 마감을 60초 뒤로 둔다.
Widget _defend(EclipseState state) => EclipseDefendScreen(
repository: FakeEclipseRepository(state: state),
fandomId: 'bts',
);
DateTime get _in60 => DateTime.now().add(const Duration(seconds: 60));
final shots = <Shot>[
// 사본 주 프레임(`252:159354`)은 헤더 지갑의 「아이템 구입」 시트가 열린 상태다.
Shot(
'F1-01',
tab: '/home',
after: (t) async {
await t.tap(find.byTooltip('아이템'));
await _settle(t);
},
),
// 기본 상태(`343:205364`) · ZAP 옵션 「아이템 선택」(`343:204868`) — 그룹 대조 폴더용.
Shot('F1-01d', tab: '/home'),
// 아래 구획(TODAY'S QUEST 등) 눈 대조용 긴 렌더.
Shot('F1-01L', tab: '/home', h: 1400),
// Figma 「홈」(`2097:16428`) 375×2904 전체 길이 — 사본 렌더와 나란히 대조.
Shot('F1-01F', tab: '/home', h: 2904),
Shot(
'F1-01i',
tab: '/home',
after: (t) async {
// 떠 있는 ZAP 은 번개만 있다(라벨 없음) — 의미 라벨로 잡는다.
final hold = await t.startGesture(
t.getCenter(find.bySemanticsLabel(RegExp(r'^ZAP'))),
);
await t.pump(const Duration(seconds: 2));
await hold.up();
await _settle(t);
await t.tap(find.text('아이템'));
await _settle(t);
},
),
Shot('F1-02', build: (r) async => const NebulaCreateScreen()),
// 성장 완료는 HOME 탭 성운 상태를 그대로 찍는다(ZAP 은 HOME 의 갤럭시 연결로 뜬다).
Shot('F1-03', tab: '/home', repos: NebulaRepos.new),
Shot('F1-04', build: (r) async => const PlanetCreateScreen()),
// 이름 입력 뒤(`340:75644`) · 색 단계(`373:358639` 의 칩 줄) — 그룹 대조 폴더에서만 쓴다.
Shot(
'F1-02n',
build: (r) async => const NebulaCreateScreen(),
after: (t) async {
await t.enterText(find.byType(TextField), 'BTS_RM');
await _settle(t);
},
),
Shot(
'F1-04c',
build: (r) async => const PlanetCreateScreen(),
after: (t) async {
await t.tap(find.text('다음'));
await _settle(t);
},
),
// 사본 주 프레임은 「+」 시트가 열린 상태다.
Shot(
'F1-05',
build: (r) async => const HomeOnboardingScreen(),
after: (t) async {
await t.tap(find.bySemanticsLabel('행성 생성 또는 가입'));
await _settle(t);
},
),
Shot(
'F7-01',
h: 815,
build: (r) async => _defend(eclipseFixture(endsAt: _in60)),
),
// 흔드는 중 — 사본 문구의 「10초」 자리는 남은 시간이다.
Shot(
'F7-02',
h: 815,
build: (r) async => _defend(
eclipseFixture(endsAt: DateTime.now().add(const Duration(seconds: 10))),
),
after: (t) async {
await t.tap(find.text('흔들기 어려우면 눌러서 참여'));
await _settle(t);
},
),
Shot(
'F7-03',
h: 815,
build: (r) async => _defend(eclipseFixture(defended: true)),
),
// 실패 변형(`494:14292`) — 공유 비교에는 없는 ID 라 그룹 대조 폴더에서만 쓴다.
Shot(
'F7-02f',
h: 815,
build: (r) async => _defend(eclipseFixture(result: EclipseResult.failure)),
),
];
Future<void> _settle(WidgetTester t) async {
for (var i = 0; i < 6; i++) {
await t.pump(const Duration(milliseconds: 300));
}
}
Future<void> _loadFonts() async {
final manifest =
json.decode(await rootBundle.loadString('FontManifest.json')) as List;
for (final font in manifest.cast<Map<String, dynamic>>()) {
final loader = FontLoader(font['family'] as String);
for (final f in (font['fonts'] as List).cast<Map<String, dynamic>>()) {
loader.addFont(rootBundle.load(Uri.decodeFull(f['asset'] as String)));
}
await loader.load();
}
await (FontLoader(
'Roboto',
)..addFont(rootBundle.load('assets/fonts/42dotSans[wght].ttf'))).load();
}
void main() {
final out = Platform.environment['DESIGN_MATCH_OUT'];
if (out == null) return;
setUpAll(() async {
Directory(out).createSync(recursive: true);
await _loadFonts();
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
messenger.setMockMethodCallHandler(
const MethodChannel('dev.fluttercommunity.plus/sensors/method'),
(_) async => null,
);
messenger.setMockStreamHandler(
const EventChannel('dev.fluttercommunity.plus/sensors/accelerometer'),
MockStreamHandler.inline(onListen: (_, _) {}),
);
});
for (final s in shots) {
testWidgets(s.id, (t) async {
t.view.devicePixelRatio = 2;
t.view.physicalSize = Size(_width * 2, s.h * 2);
t.view.padding = _padding;
t.view.viewPadding = _padding;
addTearDown(t.view.reset);
final repos = s.repos();
final boundary = GlobalKey();
final navigator = GlobalKey<NavigatorState>();
Widget? pushed;
Widget app;
if (s.tab != null) {
final router = buildRouter(
AuthRepository()..signInWithMockSession(),
repositories: repos,
);
router.go(s.tab!);
app = MaterialApp.router(
theme: buildAppTheme(),
debugShowCheckedModeBanner: false,
routerConfig: router,
);
} else {
pushed = (await t.runAsync(() => s.build!(repos)))!;
app = MaterialApp(
theme: buildAppTheme(),
debugShowCheckedModeBanner: false,
navigatorKey: navigator,
home: const SizedBox(),
);
}
await t.pumpWidget(RepaintBoundary(key: boundary, child: app));
if (pushed case final screen?) {
navigator.currentState!.push(
MaterialPageRoute<void>(builder: (_) => screen),
);
}
await _settle(t);
await s.after?.call(t);
await t.runAsync(() async {
for (final e in find.byType(Image).evaluate().toList()) {
await precacheImage((e.widget as Image).image, e);
}
});
await _settle(t);
await t.runAsync(() async {
final image =
await (boundary.currentContext!.findRenderObject()!
as RenderRepaintBoundary)
.toImage(pixelRatio: 2);
final png = await image.toByteData(format: ui.ImageByteFormat.png);
await File('$out/${s.id}.png').writeAsBytes(png!.buffer.asUint8List());
image.dispose();
});
final texts = [
for (final e in find.byType(RichText).evaluate())
if (e.renderObject case final RenderParagraph p
when p.attached && p.text.toPlainText().trim().isNotEmpty)
{
'text': p.text.toPlainText(),
'family': p.text.style?.fontFamily,
'size': p.text.style?.fontSize,
'weight': p.text.style?.fontWeight?.value,
'color': p.text.style?.color?.toARGB32().toRadixString(16),
'rect': [
for (final v in [
p.localToGlobal(Offset.zero).dx,
p.localToGlobal(Offset.zero).dy,
p.size.width,
p.size.height,
])
v.roundToDouble(),
],
},
];
File(
'$out/${s.id}.texts.json',
).writeAsStringSync(const JsonEncoder.withIndent(' ').convert(texts));
expect(t.takeException(), isNull);
});
}
}