test(client): L1 数值格式化纯函数单测 + 从 widget 抽出(支柱3)
- lib/util/format.dart:抽出 formatSpeed/formatDuration(原私有于 connect_page _speed / connect_button _fmt,混在 widget 里没法单测) - connect_page/connect_button 改用公共纯函数(渲染输出不变) - test/unit/format_test.dart:10 用例覆盖速率 B/s→GB/s 各单位边界 + 时长 HH:MM:SS 补零/小时不封顶/取模 60 - 验证:format 10 + widget 8 全过、analyze 无 error/warning Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../bridge/vpn_bridge.dart';
|
||||
import '../core/responsive/form_factor.dart';
|
||||
import '../util/format.dart';
|
||||
import '../l10n/app_text.dart';
|
||||
import '../models/node.dart';
|
||||
import '../pangolin_theme.dart';
|
||||
@@ -33,8 +34,8 @@ class ConnectPage extends ConsumerWidget {
|
||||
final isFree = ref.watch(isFreePlanProvider);
|
||||
final quota = ref.watch(quotaProvider);
|
||||
final stats = ref.watch(vpnStatsProvider).valueOrNull;
|
||||
final down = _speed(stats?.downloadSpeed);
|
||||
final up = _speed(stats?.uploadSpeed);
|
||||
final down = formatSpeed(stats?.downloadSpeed);
|
||||
final up = formatSpeed(stats?.uploadSpeed);
|
||||
// 延迟来源:连接后优先用内核 urltest 实测(代理真实 RTT);urltest 还没探过时
|
||||
// 回退用节点页的 TCP 探针 ping,尽量不显示 —。都没有才 —。
|
||||
var livePing = conn.phase == VpnPhase.on ? _bestUrltest(stats) : node.ping;
|
||||
@@ -202,17 +203,6 @@ class ConnectPage extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 字节/秒 → 动态单位 (数值, 单位):B/s · KB/s · MB/s · GB/s(小数据也看得清)。
|
||||
/// null = 未取到,显示 —。
|
||||
(String, String) _speed(double? bytesPerSec) {
|
||||
if (bytesPerSec == null) return ('—', 'KB/s');
|
||||
final b = bytesPerSec;
|
||||
if (b < 1024) return (b.toStringAsFixed(0), 'B/s');
|
||||
if (b < 1024 * 1024) return ((b / 1024).toStringAsFixed(1), 'KB/s');
|
||||
if (b < 1024 * 1024 * 1024) return ((b / (1024 * 1024)).toStringAsFixed(1), 'MB/s');
|
||||
return ((b / (1024 * 1024 * 1024)).toStringAsFixed(2), 'GB/s');
|
||||
}
|
||||
|
||||
/// 内核 urltest 各出站中的最小正延迟(ms);无则 0。
|
||||
int _bestUrltest(VpnStatsEvent? s) {
|
||||
if (s == null) return 0;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// format.dart — 纯数值格式化(支柱 3:纯逻辑与 IO 分离)。
|
||||
//
|
||||
// 从 widget 抽出的无副作用函数:不碰 BuildContext / 时钟 / 网络,纯输入→输出,
|
||||
// 可在 test/unit 里直接断言边界(见 test/unit/format_test.dart)。
|
||||
|
||||
/// 字节/秒 → 动态单位 (数值, 单位):B/s · KB/s · MB/s · GB/s(小数据也看得清)。
|
||||
/// null = 未取到 → ('—', 'KB/s')。
|
||||
(String, String) formatSpeed(double? bytesPerSec) {
|
||||
if (bytesPerSec == null) return ('—', 'KB/s');
|
||||
final b = bytesPerSec;
|
||||
if (b < 1024) return (b.toStringAsFixed(0), 'B/s');
|
||||
if (b < 1024 * 1024) return ((b / 1024).toStringAsFixed(1), 'KB/s');
|
||||
if (b < 1024 * 1024 * 1024) return ((b / (1024 * 1024)).toStringAsFixed(1), 'MB/s');
|
||||
return ((b / (1024 * 1024 * 1024)).toStringAsFixed(2), 'GB/s');
|
||||
}
|
||||
|
||||
/// 时长 → HH:MM:SS(连接计时;小时不封顶)。
|
||||
String formatDuration(Duration d) {
|
||||
String two(int n) => n.toString().padLeft(2, '0');
|
||||
return '${two(d.inHours)}:${two(d.inMinutes % 60)}:${two(d.inSeconds % 60)}';
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../pangolin_theme.dart';
|
||||
import '../state/connection_provider.dart';
|
||||
import '../util/format.dart';
|
||||
import 'pangolin_icons.dart';
|
||||
|
||||
class ConnectButton extends StatefulWidget {
|
||||
@@ -50,11 +51,6 @@ class _ConnectButtonState extends State<ConnectButton> with SingleTickerProvider
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _fmt(Duration d) {
|
||||
String two(int n) => n.toString().padLeft(2, '0');
|
||||
return '${two(d.inHours)}:${two(d.inMinutes % 60)}:${two(d.inSeconds % 60)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = context.pangolin;
|
||||
@@ -124,7 +120,7 @@ class _ConnectButtonState extends State<ConnectButton> with SingleTickerProvider
|
||||
Icon(icon, size: 50, color: fg),
|
||||
const SizedBox(height: 6),
|
||||
if (s == VpnPhase.on) ...[
|
||||
Text(_fmt(widget.elapsed),
|
||||
Text(formatDuration(widget.elapsed),
|
||||
style: PangolinText.mono
|
||||
.copyWith(color: fg, fontSize: 18, fontWeight: FontWeight.w600)),
|
||||
Text(widget.secureLabel,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// format_test.dart — L1 数值格式化纯函数单测(支柱 3:纯逻辑、确定性、零依赖)。
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pangolin_vpn/util/format.dart';
|
||||
|
||||
void main() {
|
||||
group('formatSpeed(字节/秒 → 数值+单位)', () {
|
||||
test('null → —/KB/s(未取到)', () {
|
||||
expect(formatSpeed(null), ('—', 'KB/s'));
|
||||
});
|
||||
|
||||
test('0 → 0 B/s', () {
|
||||
expect(formatSpeed(0), ('0', 'B/s'));
|
||||
});
|
||||
|
||||
test('< 1KB 用 B/s 且取整', () {
|
||||
expect(formatSpeed(512), ('512', 'B/s'));
|
||||
expect(formatSpeed(1023), ('1023', 'B/s'));
|
||||
});
|
||||
|
||||
test('KB/s 边界(1024)与一位小数', () {
|
||||
expect(formatSpeed(1024), ('1.0', 'KB/s'));
|
||||
expect(formatSpeed(1536), ('1.5', 'KB/s'));
|
||||
});
|
||||
|
||||
test('MB/s 边界(1024^2)', () {
|
||||
expect(formatSpeed(1024 * 1024), ('1.0', 'MB/s'));
|
||||
expect(formatSpeed(1024 * 1024 * 5 + 512 * 1024), ('5.5', 'MB/s'));
|
||||
});
|
||||
|
||||
test('GB/s 边界(1024^3)与两位小数', () {
|
||||
expect(formatSpeed(1024 * 1024 * 1024), ('1.00', 'GB/s'));
|
||||
expect(formatSpeed(2.5 * 1024 * 1024 * 1024), ('2.50', 'GB/s'));
|
||||
});
|
||||
});
|
||||
|
||||
group('formatDuration(时长 → HH:MM:SS)', () {
|
||||
test('0 → 00:00:00', () {
|
||||
expect(formatDuration(Duration.zero), '00:00:00');
|
||||
});
|
||||
|
||||
test('秒/分补零', () {
|
||||
expect(formatDuration(const Duration(seconds: 5)), '00:00:05');
|
||||
expect(formatDuration(const Duration(minutes: 3, seconds: 9)), '00:03:09');
|
||||
});
|
||||
|
||||
test('小时不封顶(对齐连接计时 01:04:59)', () {
|
||||
expect(
|
||||
formatDuration(const Duration(hours: 1, minutes: 4, seconds: 59)),
|
||||
'01:04:59',
|
||||
);
|
||||
expect(formatDuration(const Duration(hours: 100)), '100:00:00');
|
||||
});
|
||||
|
||||
test('分/秒取模 60(不溢出进位错乱)', () {
|
||||
expect(formatDuration(const Duration(minutes: 90)), '01:30:00');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user