271 lines
8.7 KiB
Dart
271 lines
8.7 KiB
Dart
@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),
|
|
Shot(
|
|
'F1-01i',
|
|
tab: '/home',
|
|
after: (t) async {
|
|
final hold = await t.startGesture(t.getCenter(find.text('기여')));
|
|
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);
|
|
});
|
|
}
|
|
}
|