2026-09-21 14:55:29 +08:00
|
|
|
|
/// UI-01 主题一致性测试(Task 3)。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 覆盖:浅色 ColorScheme 方案 §3.1 精确值、深色语义对应值、字阶
|
2026-09-22 00:28:39 +08:00
|
|
|
|
/// (UI-03 §3 校准:24/17/16-14/13-12)、组件主题(按钮高 48/圆角 12、
|
|
|
|
|
|
/// 输入圆角 12、Dialog 圆角 16、BottomSheet 顶部圆角 16、AppBar
|
|
|
|
|
|
/// titleLarge)、动效令牌(含「减少动态效果」归零)。
|
2026-09-21 14:55:29 +08:00
|
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
|
import 'package:photo_live/app/theme.dart';
|
|
|
|
|
|
import 'package:photo_live/ui/tokens.dart';
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
|
group('浅色 ColorScheme(方案 §3.1 精确值)', () {
|
|
|
|
|
|
final scheme = AppTheme.light.colorScheme;
|
|
|
|
|
|
|
|
|
|
|
|
test('基础色', () {
|
|
|
|
|
|
expect(scheme.primary, const Color(0xFF1A73E8));
|
|
|
|
|
|
expect(scheme.onPrimary, const Color(0xFFFFFFFF));
|
|
|
|
|
|
expect(scheme.surface, const Color(0xFFFFFFFF));
|
|
|
|
|
|
expect(scheme.onSurface, const Color(0xFF172033));
|
|
|
|
|
|
expect(scheme.secondary, const Color(0xFF996300), reason: '等待色');
|
|
|
|
|
|
expect(scheme.error, const Color(0xFFB42318));
|
|
|
|
|
|
// 成功色不进 ColorScheme(独立令牌)。
|
|
|
|
|
|
expect(kColorSuccess, const Color(0xFF16794B));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('页面背景与分隔线/次文字', () {
|
|
|
|
|
|
expect(AppTheme.light.scaffoldBackgroundColor, const Color(0xFFF6F8FB));
|
|
|
|
|
|
expect(AppTheme.light.dividerTheme.color, kColorDivider);
|
|
|
|
|
|
expect(kColorDivider, const Color(0xFFE5EAF1));
|
|
|
|
|
|
expect(scheme.onSurfaceVariant, kColorTextSecondary);
|
2026-09-23 08:09:20 +08:00
|
|
|
|
expect(kColorTextSecondary, const Color(0xFF69778E)); // UI06: app-preview --muted
|
2026-09-21 14:55:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('深色 ColorScheme(语义对应,不反转图片)', () {
|
|
|
|
|
|
final theme = AppTheme.dark;
|
|
|
|
|
|
final scheme = theme.colorScheme;
|
|
|
|
|
|
|
|
|
|
|
|
test('深表面 + 浅文字 + 提亮主色', () {
|
|
|
|
|
|
expect(scheme.brightness, Brightness.dark);
|
|
|
|
|
|
expect(scheme.surface, const Color(0xFF1F2530));
|
|
|
|
|
|
expect(scheme.onSurface, const Color(0xFFE6EAF2));
|
|
|
|
|
|
expect(scheme.primary, kColorInfoDark);
|
|
|
|
|
|
expect(scheme.onPrimary, const Color(0xFF0B2E6F));
|
|
|
|
|
|
expect(theme.scaffoldBackgroundColor, const Color(0xFF12161D));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('等待/错误为深色提亮变体', () {
|
|
|
|
|
|
expect(scheme.secondary, kColorWaitingDark);
|
|
|
|
|
|
expect(scheme.error, kColorErrorDark);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('字阶(随系统缩放:fontSize 走 TextScaler)', () {
|
|
|
|
|
|
final textTheme = AppTheme.light.textTheme;
|
|
|
|
|
|
|
2026-09-22 00:28:39 +08:00
|
|
|
|
test('24 页面标题 / 17 分组 / 16-14 正文 / 13-12 辅助', () {
|
|
|
|
|
|
expect(textTheme.titleLarge!.fontSize, 24);
|
2026-09-21 14:55:29 +08:00
|
|
|
|
expect(textTheme.titleMedium!.fontSize, 17);
|
|
|
|
|
|
expect(textTheme.bodyLarge!.fontSize, 16);
|
|
|
|
|
|
expect(textTheme.bodyMedium!.fontSize, 14);
|
|
|
|
|
|
expect(textTheme.bodySmall!.fontSize, 13);
|
|
|
|
|
|
expect(textTheme.labelSmall!.fontSize, 12);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('AppBar 标题用 titleLarge', () {
|
2026-09-22 00:28:39 +08:00
|
|
|
|
expect(AppTheme.light.appBarTheme.titleTextStyle!.fontSize, 24);
|
2026-09-21 14:55:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('组件主题', () {
|
|
|
|
|
|
final theme = AppTheme.light;
|
|
|
|
|
|
|
|
|
|
|
|
test('按钮高 48、圆角 12', () {
|
|
|
|
|
|
final filled = theme.filledButtonTheme.style;
|
|
|
|
|
|
expect(
|
|
|
|
|
|
filled!.minimumSize!.resolve(const <WidgetState>{}),
|
|
|
|
|
|
const Size(64, 48),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
theme.elevatedButtonTheme.style!.minimumSize!.resolve(
|
|
|
|
|
|
const <WidgetState>{},
|
|
|
|
|
|
),
|
|
|
|
|
|
const Size(64, 48),
|
|
|
|
|
|
);
|
|
|
|
|
|
final shape =
|
|
|
|
|
|
filled.shape!.resolve(const <WidgetState>{})
|
|
|
|
|
|
as RoundedRectangleBorder;
|
|
|
|
|
|
expect(shape.borderRadius, BorderRadius.circular(kRadiusInput));
|
|
|
|
|
|
expect(
|
|
|
|
|
|
theme.outlinedButtonTheme.style!.minimumSize!.resolve(
|
|
|
|
|
|
const <WidgetState>{},
|
|
|
|
|
|
),
|
|
|
|
|
|
const Size(48, 48),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
theme.textButtonTheme.style!.minimumSize!.resolve(
|
|
|
|
|
|
const <WidgetState>{},
|
|
|
|
|
|
),
|
|
|
|
|
|
const Size(48, 48),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-23 08:09:20 +08:00
|
|
|
|
test('输入框圆角 10(UI06: app-preview .field)', () {
|
2026-09-21 14:55:29 +08:00
|
|
|
|
final border = theme.inputDecorationTheme.border as OutlineInputBorder;
|
2026-09-23 08:09:20 +08:00
|
|
|
|
expect(border.borderRadius, BorderRadius.circular(kRadiusField));
|
|
|
|
|
|
expect(kRadiusField, 10);
|
2026-09-21 14:55:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Dialog 圆角 16、BottomSheet 顶部圆角 16', () {
|
|
|
|
|
|
final dialog = theme.dialogTheme.shape as RoundedRectangleBorder;
|
|
|
|
|
|
expect(dialog.borderRadius, BorderRadius.circular(kRadiusCard));
|
|
|
|
|
|
final sheet = theme.bottomSheetTheme.shape as RoundedRectangleBorder;
|
|
|
|
|
|
expect(
|
|
|
|
|
|
(sheet.borderRadius as BorderRadius).topLeft,
|
|
|
|
|
|
const Radius.circular(kRadiusCard),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
(sheet.borderRadius as BorderRadius).topRight,
|
|
|
|
|
|
const Radius.circular(kRadiusCard),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(kRadiusCard, 16);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-09-23 08:09:20 +08:00
|
|
|
|
test('底部导航无胶囊指示器(UI06: app-preview .nav)', () {
|
|
|
|
|
|
expect(theme.navigationBarTheme.indicatorColor, Colors.transparent);
|
2026-09-21 14:55:29 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('动效令牌', () {
|
|
|
|
|
|
testWidgets('默认轻量 180ms / 轮播 300ms / 减少动态效果归零', (tester) async {
|
|
|
|
|
|
expect(kMotionFast, const Duration(milliseconds: 180));
|
|
|
|
|
|
expect(kMotionCarousel, const Duration(milliseconds: 300));
|
|
|
|
|
|
expect(kMotionReduce, Duration.zero);
|
|
|
|
|
|
|
|
|
|
|
|
late BuildContext captured;
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
MaterialApp(
|
|
|
|
|
|
home: Builder(
|
|
|
|
|
|
builder: (context) {
|
|
|
|
|
|
captured = context;
|
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(motion(captured), kMotionFast);
|
|
|
|
|
|
expect(motion(captured, carousel: true), kMotionCarousel);
|
|
|
|
|
|
|
|
|
|
|
|
// 「减少动态效果」开启 → 一律归零。
|
|
|
|
|
|
await tester.pumpWidget(
|
|
|
|
|
|
MaterialApp(
|
|
|
|
|
|
home: MediaQuery(
|
|
|
|
|
|
data: const MediaQueryData(disableAnimations: true),
|
|
|
|
|
|
child: Builder(
|
|
|
|
|
|
builder: (context) {
|
|
|
|
|
|
captured = context;
|
|
|
|
|
|
return const SizedBox.shrink();
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(motion(captured), kMotionReduce);
|
|
|
|
|
|
expect(motion(captured, carousel: true), kMotionReduce);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|