Files
pangolin/client/test/golden/tablet_pages_golden_test.dart
T
wangjia 4f9d2d2cf3 fix(client): 补提交 stats-overhaul 漏掉的 account_api/kernel_process WIP
同 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>
2026-06-28 17:53:56 +08:00

163 lines
7.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.
// 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:falsepump 固定帧)。
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));
}