Files
jiu/client/test/support/shell_harness.dart
T

166 lines
5.6 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.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:jiu_client/core/auth/auth_state.dart';
import 'package:jiu_client/core/theme/themes.dart';
import 'package:jiu_client/models/shop.dart';
import 'package:jiu_client/providers/shop_provider.dart';
import 'package:jiu_client/providers/license_provider.dart';
import 'package:jiu_client/providers/update_provider.dart';
import 'package:jiu_client/providers/connectivity_provider.dart';
import 'package:jiu_client/providers/session_heartbeat.dart';
import 'package:jiu_client/screens/shell/app_shell.dart';
import 'package:jiu_client/widgets/app_status_bar.dart';
import 'golden_harness.dart';
/// 外壳挂载版 golden 骨架:把屏放进真 AppShell(顶栏+侧栏+状态栏)渲染,
/// 与原型整页截图(fidelity.mjs 的 proto 基准)同构,可直接整屏 diff。
/// 门店/用户/版本对齐原型:鼎晟酒行/DSJH-001、王经理(管理员)、v1.0.72。
class _FakeAuth extends AuthNotifier {
_FakeAuth() {
state = const AuthState(
user: AuthUser(
accessToken: 't',
refreshToken: 'r',
id: 1,
username: 'wang',
realName: '王经理',
shopNo: 'DSJH-001',
shopId: 1,
role: 'admin'),
);
}
}
class _FakeUpdate extends UpdateNotifier {
@override
Future<AppUpdateInfo?> build() async => null;
}
class _FakeLicense extends LicenseNotifier {
@override
Future<LicenseInfo?> build() async => null;
}
const _shop = ShopInfo(
id: 1,
code: 'DSJH-001',
name: '鼎晟酒行',
address: '浙江省杭州市',
phone: '0571-88886666',
managerName: '王经理');
/// 外壳自身依赖的 provider 覆盖(认证/门店/授权/更新/网络/心跳/版本)。
List<Override> shellOverrides() => [
authStateProvider.overrideWith((ref) => _FakeAuth()),
shopInfoProvider.overrideWith((ref) => _shop),
licenseProvider.overrideWith(() => _FakeLicense()),
updateProvider.overrideWith(() => _FakeUpdate()),
// 跳过联网首检 / 不启动心跳,避免后台 Timer 泄漏
connectivityProvider
.overrideWith((ref) => ConnectivityNotifier(skipInit: true)),
sessionHeartbeatProvider.overrideWith((ref) => SessionHeartbeat(ref)),
appVersionProvider.overrideWith((ref) => 'v1.0.72'),
isReadonlyProvider.overrideWithValue(false),
];
// 外壳 nav 引用的分支序号需与 app_router.dart 一一对齐(0-8 侧栏 + 9 我的
// 占位 + 10 授权管理),否则侧栏高亮(_navGroups item.index)会错位。
StatefulShellBranch _branch(String path, [WidgetBuilder? builder]) =>
StatefulShellBranch(routes: [
GoRoute(
path: path,
builder: (ctx, _) => builder?.call(ctx) ?? const SizedBox.expand()),
]);
GoRouter _router(String initialPath, WidgetBuilder screen) => GoRouter(
initialLocation: initialPath,
routes: [
StatefulShellRoute.indexedStack(
builder: (ctx, state, shell) =>
AppShell(navigationShell: shell, location: state.matchedLocation),
branches: [
for (final p in const [
'/stock-in',
'/stock-out',
'/inventory',
'/finance',
'/partners',
'/products',
'/devices',
'/settings',
'/about',
'/me', // 9:桌面侧栏不含此项,占位保持分支序号对齐
'/license', // 10:授权管理独立一级屏
])
_branch(p, p == initialPath ? screen : null),
],
),
],
);
/// 在指定主题下把 [screen] 挂进外壳渲染并比对 golden1280×900@2x 对齐原型)。
Future<void> pumpShellGolden(
WidgetTester tester, {
required String path,
required WidgetBuilder screen,
required String theme,
required String goldenName,
List<Override> overrides = const [],
Size logical = const Size(1280, 900),
double dpr = 2.0,
}) async {
await ensureGoldenFonts();
// 冻结状态栏时钟,确定化 golden(与 app_shell golden 同基准时刻)。
AppStatusBar.clock = () => DateTime(2026, 7, 13, 9, 30, 15);
AppStatusBar.ticking = false;
tester.view.physicalSize = Size(logical.width * dpr, logical.height * dpr);
tester.view.devicePixelRatio = dpr;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(ProviderScope(
overrides: [...shellOverrides(), ...overrides],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
theme: withGoldenFont(buildTheme(theme)),
routerConfig: _router(path, screen),
),
));
await tester.pumpAndSettle();
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('goldens/$goldenName.png'),
);
}
/// 「同一屏 × A/B/C 三主题」外壳挂载 golden 组。
void shellGoldenAcrossThemes(
String description, {
required String path,
required WidgetBuilder screen,
required String goldenPrefix,
List<Override> Function()? overrides,
Size logical = const Size(1280, 900),
double dpr = 2.0,
}) {
for (final theme in const ['a', 'b', 'c']) {
testWidgets('$description · theme $theme', (tester) async {
await pumpShellGolden(
tester,
path: path,
screen: screen,
theme: theme,
goldenName: '${goldenPrefix}_$theme',
overrides: overrides?.call() ?? const [],
logical: logical,
dpr: dpr,
);
});
}
}