4f9d2d2cf3
同 device_usage:account_api.dart(deviceUsage/usage 方法)、kernel_process.dart 及对应测试一直未提交,本地有→analyze/test 过,但不在 git→Windows 构建报 「deviceUsage isn't defined」。补齐使提交树自洽可构建。 孤儿组件 device_stat_row/metric_card 已无人引用,不提交。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
7.0 KiB
Dart
163 lines
7.0 KiB
Dart
// tablet_pages_golden_test.dart — iPad/tablet 整页 golden(连接/节点/统计/账户)
|
||
//
|
||
// 用 debugDefaultTargetPlatformOverride=iOS + 1180×820 surface 让 context.formFactor
|
||
// 判定为 tablet(宽 ≥900),渲染 TabletShell 各页,对照 design/ui_kits/tablet/tabapp.jsx
|
||
// 像素基准做保真校准 / 回归闸。注入演示数据(节点/用量/账户)让网格与柱图有内容;
|
||
// 覆盖矩阵 = 四视图 × {light,dark} × {zh,en}。生成基准:flutter test --update-goldens。
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart';
|
||
import 'package:pangolin_vpn/bridge/vpn_bridge_provider.dart';
|
||
import 'package:pangolin_vpn/l10n/app_text.dart';
|
||
import 'package:pangolin_vpn/models/device_usage.dart';
|
||
import 'package:pangolin_vpn/models/me.dart';
|
||
import 'package:pangolin_vpn/models/node.dart';
|
||
import 'package:pangolin_vpn/models/usage_point.dart';
|
||
import 'package:pangolin_vpn/pangolin_theme.dart';
|
||
import 'package:pangolin_vpn/services/token_store.dart';
|
||
import 'package:pangolin_vpn/shell/home_shell.dart';
|
||
import 'package:pangolin_vpn/shell/tablet_shell.dart';
|
||
import 'package:pangolin_vpn/state/account_providers.dart';
|
||
import 'package:pangolin_vpn/state/app_providers.dart';
|
||
import 'package:pangolin_vpn/state/auth_provider.dart';
|
||
import 'package:pangolin_vpn/state/navigation_provider.dart';
|
||
import 'package:pangolin_vpn/state/nodes_provider.dart';
|
||
|
||
class _NullTokenStore implements TokenStore {
|
||
const _NullTokenStore();
|
||
@override
|
||
Future<void> saveTokens({required String access, required String refresh}) async {}
|
||
@override
|
||
Future<String?> loadAccessToken() async => null;
|
||
@override
|
||
Future<String?> loadRefreshToken() async => null;
|
||
@override
|
||
Future<void> clear() async {}
|
||
@override
|
||
Future<void> markOnboarded() async {}
|
||
@override
|
||
Future<bool> isOnboarded() async => true;
|
||
@override
|
||
Future<void> saveLastEmail(String email) async {}
|
||
@override
|
||
Future<String?> loadLastEmail() async => null;
|
||
}
|
||
|
||
// ── 演示数据(让网格/柱图/账户有内容;不依赖登录态与网络)─────────────
|
||
const List<Node> _demoNodes = [
|
||
Node(code: 'HK', nameZh: '香港 · 流媒体', nameEn: 'Hong Kong', ping: 18, tag: NodeTag.streaming, host: 'hk', port: 443),
|
||
Node(code: 'TW', nameZh: '台北', nameEn: 'Taipei', ping: 28, host: 'tw', port: 443),
|
||
Node(code: 'JP', nameZh: '东京', nameEn: 'Tokyo', ping: 32, host: 'jp', port: 443),
|
||
Node(code: 'KR', nameZh: '首尔', nameEn: 'Seoul', ping: 38, host: 'kr', port: 443),
|
||
Node(code: 'SG', nameZh: '新加坡', nameEn: 'Singapore', ping: 45, tag: NodeTag.p2p, host: 'sg', port: 443),
|
||
Node(code: 'US', nameZh: '洛杉矶', nameEn: 'Los Angeles', ping: 120, host: 'us', port: 443),
|
||
];
|
||
|
||
const Me _demoMe = Me(
|
||
email: 'me@pangolin.vpn',
|
||
plan: 'free',
|
||
devicesUsed: 2,
|
||
devicesMax: 1,
|
||
quotaTodayMin: 6,
|
||
dataTodayGb: 1.2,
|
||
weeklyGb: [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0],
|
||
);
|
||
|
||
final List<UsagePoint> _demoUsage = List.generate(30, (i) {
|
||
return UsagePoint(
|
||
date: '2026-06-${(i + 1).toString().padLeft(2, '0')}',
|
||
bytesUp: 120 * 1024 * 1024 + i * 7 * 1024 * 1024,
|
||
bytesDown: 900 * 1024 * 1024 + i * 31 * 1024 * 1024,
|
||
minutesUsed: 24 + (i % 5) * 11,
|
||
);
|
||
});
|
||
|
||
// 分设备用量演示数据(统计页「设备明细」section):三台设备,流量递减以验证占比条。
|
||
const List<DeviceUsage> _demoDeviceUsage = [
|
||
DeviceUsage(uuid: 'd1', name: 'iPhone 15', platform: 'ios', bytesUp: 1500 * 1024 * 1024, bytesDown: 9800 * 1024 * 1024, minutesUsed: 312),
|
||
DeviceUsage(uuid: 'd2', name: 'MacBook Pro', platform: 'macos', bytesUp: 800 * 1024 * 1024, bytesDown: 5200 * 1024 * 1024, minutesUsed: 188),
|
||
DeviceUsage(uuid: 'd3', name: 'Pixel 8', platform: 'android', bytesUp: 200 * 1024 * 1024, bytesDown: 1400 * 1024 * 1024, minutesUsed: 64),
|
||
];
|
||
|
||
class _FakeNodesNotifier extends NodesNotifier {
|
||
@override
|
||
Future<List<Node>> build() async => _demoNodes;
|
||
}
|
||
|
||
class _FakeMeNotifier extends MeNotifier {
|
||
@override
|
||
Future<Me> build() async => _demoMe;
|
||
}
|
||
|
||
const Size _ipad = Size(1180, 820); // 对齐设计源 tabapp.jsx 内屏
|
||
|
||
Future<void> _shoot(
|
||
WidgetTester tester,
|
||
NavView view,
|
||
String name, {
|
||
bool settle = true,
|
||
Brightness brightness = Brightness.light,
|
||
AppLang lang = AppLang.zh,
|
||
}) async {
|
||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||
tester.view.physicalSize = _ipad;
|
||
tester.view.devicePixelRatio = 1.0;
|
||
addTearDown(tester.view.resetPhysicalSize);
|
||
addTearDown(tester.view.resetDevicePixelRatio);
|
||
try {
|
||
await tester.pumpWidget(ProviderScope(
|
||
overrides: [
|
||
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
|
||
vpnBridgeProvider.overrideWithValue(VpnBridgeMock()),
|
||
navViewProvider.overrideWith((ref) => view),
|
||
localeProvider.overrideWith((ref) => lang),
|
||
nodesProvider.overrideWith(_FakeNodesNotifier.new),
|
||
meProvider.overrideWith(_FakeMeNotifier.new),
|
||
usageProvider(30).overrideWith((ref) async => _demoUsage),
|
||
deviceUsageProvider(30).overrideWith((ref) async => _demoDeviceUsage),
|
||
],
|
||
child: MaterialApp(
|
||
debugShowCheckedModeBanner: false,
|
||
theme: brightness == Brightness.dark ? PangolinTheme.dark : PangolinTheme.light,
|
||
home: const HomeShell(),
|
||
),
|
||
));
|
||
if (settle) {
|
||
await tester.pumpAndSettle();
|
||
} else {
|
||
await tester.pump();
|
||
await tester.pump(const Duration(milliseconds: 300));
|
||
}
|
||
await expectLater(find.byType(TabletShell), matchesGoldenFile('goldens/$name.png'));
|
||
} finally {
|
||
debugDefaultTargetPlatformOverride = null;
|
||
}
|
||
}
|
||
|
||
void main() {
|
||
// 连接页 off 态含连接键虚线环静态,但稳妥起见用 settle:false(pump 固定帧)。
|
||
for (final lang in const [AppLang.zh, AppLang.en]) {
|
||
final l = lang == AppLang.zh ? 'zh' : 'en';
|
||
testWidgets('tablet 连接·light·$l',
|
||
(t) => _shoot(t, NavView.connect, 'tablet_connect_light_$l', settle: false, lang: lang));
|
||
testWidgets('tablet 节点·light·$l',
|
||
(t) => _shoot(t, NavView.servers, 'tablet_servers_light_$l', lang: lang));
|
||
testWidgets('tablet 统计·light·$l',
|
||
(t) => _shoot(t, NavView.stats, 'tablet_stats_light_$l', lang: lang));
|
||
testWidgets('tablet 账户·light·$l',
|
||
(t) => _shoot(t, NavView.account, 'tablet_account_light_$l', lang: lang));
|
||
}
|
||
|
||
// 深色覆盖(中文,验证暗主题各视图)。
|
||
testWidgets('tablet 连接·dark·zh',
|
||
(t) => _shoot(t, NavView.connect, 'tablet_connect_dark_zh', settle: false, brightness: Brightness.dark));
|
||
testWidgets('tablet 节点·dark·zh',
|
||
(t) => _shoot(t, NavView.servers, 'tablet_servers_dark_zh', brightness: Brightness.dark));
|
||
testWidgets('tablet 统计·dark·zh',
|
||
(t) => _shoot(t, NavView.stats, 'tablet_stats_dark_zh', brightness: Brightness.dark));
|
||
testWidgets('tablet 账户·dark·zh',
|
||
(t) => _shoot(t, NavView.account, 'tablet_account_dark_zh', brightness: Brightness.dark));
|
||
}
|