Compare commits
No commits in common. "bcca8432f9abaf2a6f225dd3f55f942e55a274e1" and "095e9cc464f8a3d11ad83e84f5ef58a3c91ad7f7" have entirely different histories.
bcca8432f9
...
095e9cc464
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -5,19 +5,6 @@
|
|||
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
此项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html) 规范。
|
||||
|
||||
## [1.1.0] - 2026-01-20
|
||||
|
||||
### 新增 (New)
|
||||
- **密码保护 (Password Protection)**: 支持设置 `password` 为面板添加安全锁,防止非授权查看。
|
||||
- **内置拦截器 (Built-in Interceptor)**: 新增 `YxNetInspectorDioInterceptor`,一行代码集成 Dio。
|
||||
- **cURL 支持**: 详情页新增 cURL 命令生成与复制功能。
|
||||
- **Headers 详情**: 详情页新增独立的请求/响应 Headers 展示卡片。
|
||||
- **API 增强**: `logResponse` 新增 `responseHeaders` 参数。
|
||||
|
||||
### 优化 (Improvements)
|
||||
- 优化长文本展示,支持自动换行与滚动。
|
||||
- 修复非全屏模式下 Header 溢出问题。
|
||||
|
||||
## [1.0.0] - 2024-12-20
|
||||
|
||||
### 新增
|
||||
|
|
|
|||
|
|
@ -41,22 +41,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
dio:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -155,14 +139,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -262,7 +238,7 @@ packages:
|
|||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.4"
|
||||
version: "1.0.3"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,11 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
|
||||
/// 配置信息
|
||||
late YxNetInspectorConfig _config;
|
||||
YxNetInspectorConfig get config => _config;
|
||||
|
||||
/// 网络日志列表
|
||||
final List<NetworkLogEntry> _logs = <NetworkLogEntry>[];
|
||||
List<NetworkLogEntry> get logs => List.unmodifiable(_logs);
|
||||
|
||||
/// 统计数据
|
||||
int _requestCount = 0;
|
||||
|
|
@ -31,54 +33,16 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
|
||||
/// 悬浮球显示状态
|
||||
bool _showFloatingBall = true;
|
||||
bool get showFloatingBall => _showFloatingBall && _config.isEnabled;
|
||||
|
||||
/// 面板显示状态
|
||||
bool _isPanelVisible = false;
|
||||
bool _isUnlocked = true; // 默认为已解锁
|
||||
bool get isPanelVisible => _isPanelVisible && _config.isEnabled;
|
||||
|
||||
/// 初始化控制器
|
||||
/// 初始化控制器配置
|
||||
void initialize(YxNetInspectorConfig config) {
|
||||
_config = config;
|
||||
_showFloatingBall = config.showFloatingBall;
|
||||
|
||||
// 如果配置了密码,初始化为锁定状态
|
||||
if (config.password != null && config.password!.isNotEmpty) {
|
||||
_isUnlocked = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前配置
|
||||
YxNetInspectorConfig get config => _config;
|
||||
|
||||
/// 获取日志列表
|
||||
List<NetworkLogEntry> get logs => List.unmodifiable(_logs);
|
||||
|
||||
/// 获取悬浮球可见性
|
||||
bool get showFloatingBall => _showFloatingBall;
|
||||
|
||||
/// 获取面板可见性
|
||||
bool get isPanelVisible => _isPanelVisible;
|
||||
|
||||
/// 获取是否已解锁
|
||||
bool get isUnlocked => _isUnlocked;
|
||||
|
||||
/// 尝试解锁
|
||||
/// 返回是否成功
|
||||
Future<bool> unlock(String password) async {
|
||||
if (_config.password == password) {
|
||||
_isUnlocked = true;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 锁定检查器
|
||||
void lock() {
|
||||
if (_config.password != null) {
|
||||
_isUnlocked = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示面板
|
||||
|
|
@ -133,7 +97,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
int? statusCode,
|
||||
dynamic responseData,
|
||||
Duration? duration,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
}) {
|
||||
if (!_config.isEnabled) return;
|
||||
|
||||
|
|
@ -147,7 +110,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
final updatedLog = originalLog.copyWith(
|
||||
statusCode: statusCode,
|
||||
responseData: responseData,
|
||||
responseHeaders: responseHeaders,
|
||||
duration: duration,
|
||||
isSuccess: isSuccess,
|
||||
status: isSuccess
|
||||
|
|
@ -273,43 +235,6 @@ class YxNetInspectorController extends ChangeNotifier {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
/// 获取所有日志的字符串表示 (用于导出/分享)
|
||||
String getAllLogsAsString() {
|
||||
final buffer = StringBuffer();
|
||||
buffer.writeln('YxNetInspector Exported Logs');
|
||||
buffer.writeln('Generated at: ${DateTime.now()}');
|
||||
buffer.writeln('Total Requests: $requestCount');
|
||||
buffer.writeln('Success: $successCount, Failed: $_errorCount');
|
||||
buffer.writeln('----------------------------------------');
|
||||
|
||||
for (final log in _logs) {
|
||||
buffer.writeln('[${log.method}] ${log.url}');
|
||||
buffer.writeln('Status: ${log.statusCode ?? "N/A"}');
|
||||
buffer.writeln('Time: ${log.timestamp}');
|
||||
buffer.writeln('Duration: ${log.formattedDuration}');
|
||||
|
||||
if (log.headers != null && log.headers!.isNotEmpty) {
|
||||
buffer.writeln('Headers: ${log.headers}');
|
||||
}
|
||||
|
||||
if (log.requestData != null) {
|
||||
buffer.writeln('Request Data: ${log.requestData}');
|
||||
}
|
||||
|
||||
if (log.responseData != null) {
|
||||
buffer.writeln('Response Data: ${log.responseData}');
|
||||
}
|
||||
|
||||
if (log.errorMessage != null) {
|
||||
buffer.writeln('Error: ${log.errorMessage}');
|
||||
}
|
||||
|
||||
buffer.writeln('----------------------------------------');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 限制日志数量
|
||||
void _trimLogs() {
|
||||
if (_logs.length > _config.maxLogs) {
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:yx_net_inspector/src/yx_net_inspector_app.dart';
|
||||
|
||||
/// 网络检查器 Dio 拦截器
|
||||
///
|
||||
/// 用于自动记录 Dio 请求和响应日志到 YxNetInspector
|
||||
class YxNetInspectorDioInterceptor extends Interceptor {
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
// 记录请求
|
||||
YxNetInspectorGlobal.logRequest(
|
||||
id: options.hashCode.toString(),
|
||||
method: options.method,
|
||||
url: options.uri.toString(),
|
||||
headers: options.headers,
|
||||
requestData: options.data,
|
||||
queryParameters: options.queryParameters,
|
||||
);
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(
|
||||
Response<dynamic> response, ResponseInterceptorHandler handler,) {
|
||||
// 记录响应
|
||||
try {
|
||||
// 计算请求耗时(如果可能)
|
||||
// 注意:Dio 的 RequestOptions 不带时间戳,我们这里可能依赖 Controller 内部的计时,
|
||||
// 或者我们可以扩展 RequestOptions 来携带时间戳 (extra)
|
||||
final requestOptions = response.requestOptions;
|
||||
|
||||
YxNetInspectorGlobal.logResponse(
|
||||
id: requestOptions.hashCode.toString(),
|
||||
statusCode: response.statusCode,
|
||||
responseData: response.data,
|
||||
responseHeaders: response.headers.map,
|
||||
// 这里没有准确的耗时,因为 logRequest 记录了开始时间。
|
||||
// Controller 会根据 ID 自动计算耗时:Duration = Now - StartTime
|
||||
);
|
||||
} on Object catch (_) {
|
||||
// 忽略日志记录错误,避免影响业务
|
||||
}
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
// 记录错误
|
||||
try {
|
||||
final requestOptions = err.requestOptions;
|
||||
|
||||
YxNetInspectorGlobal.logError(
|
||||
id: requestOptions.hashCode.toString(),
|
||||
error: err.message ?? err.toString(),
|
||||
statusCode: err.response?.statusCode,
|
||||
);
|
||||
} on Object catch (_) {
|
||||
// 忽略日志记录错误
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ class YxNetInspectorConfig {
|
|||
this.draggable = true,
|
||||
this.showBadge = true,
|
||||
this.autoHide = false,
|
||||
this.password,
|
||||
});
|
||||
|
||||
/// 是否显示悬浮球
|
||||
|
|
@ -48,9 +47,6 @@ class YxNetInspectorConfig {
|
|||
/// 是否自动隐藏悬浮球
|
||||
final bool autoHide;
|
||||
|
||||
/// 访问检查器面板所需的密码(如果为空则无需密码)
|
||||
final String? password;
|
||||
|
||||
/// 根据当前模式判断检查器是否应该启用
|
||||
bool get isEnabled {
|
||||
if (kDebugMode) {
|
||||
|
|
@ -72,7 +68,6 @@ class YxNetInspectorConfig {
|
|||
bool? draggable,
|
||||
bool? showBadge,
|
||||
bool? autoHide,
|
||||
String? password,
|
||||
}) {
|
||||
return YxNetInspectorConfig(
|
||||
showFloatingBall: showFloatingBall ?? this.showFloatingBall,
|
||||
|
|
@ -85,7 +80,6 @@ class YxNetInspectorConfig {
|
|||
draggable: draggable ?? this.draggable,
|
||||
showBadge: showBadge ?? this.showBadge,
|
||||
autoHide: autoHide ?? this.autoHide,
|
||||
password: password ?? this.password,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -101,8 +95,7 @@ class YxNetInspectorConfig {
|
|||
'initialPosition: $initialPosition, '
|
||||
'draggable: $draggable, '
|
||||
'showBadge: $showBadge, '
|
||||
'autoHide: $autoHide, '
|
||||
'password: ${password != null ? "***" : "null"}'
|
||||
'autoHide: $autoHide'
|
||||
')';
|
||||
}
|
||||
|
||||
|
|
@ -119,8 +112,7 @@ class YxNetInspectorConfig {
|
|||
other.initialPosition == initialPosition &&
|
||||
other.draggable == draggable &&
|
||||
other.showBadge == showBadge &&
|
||||
other.autoHide == autoHide &&
|
||||
other.password == password;
|
||||
other.autoHide == autoHide;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -136,7 +128,6 @@ class YxNetInspectorConfig {
|
|||
draggable,
|
||||
showBadge,
|
||||
autoHide,
|
||||
password,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 网络请求状态枚举
|
||||
|
|
@ -24,7 +22,6 @@ class NetworkLogEntry {
|
|||
required this.timestamp,
|
||||
required this.isSuccess,
|
||||
this.headers,
|
||||
this.responseHeaders,
|
||||
this.requestData,
|
||||
this.queryParameters,
|
||||
this.statusCode,
|
||||
|
|
@ -37,7 +34,6 @@ class NetworkLogEntry {
|
|||
final String method;
|
||||
final String url;
|
||||
final Map<String, dynamic>? headers;
|
||||
final Map<String, dynamic>? responseHeaders;
|
||||
final dynamic requestData;
|
||||
final Map<String, dynamic>? queryParameters;
|
||||
final int? statusCode;
|
||||
|
|
@ -129,39 +125,12 @@ class NetworkLogEntry {
|
|||
}
|
||||
}
|
||||
|
||||
/// 生成 cURL 命令用于调试 (跨平台兼容: macOS/Linux/Windows)
|
||||
String toCurlCommand() {
|
||||
final buffer = StringBuffer('curl -X $method "$url"');
|
||||
headers?.forEach((key, value) {
|
||||
// 转义双引号以兼容所有平台
|
||||
final escapedValue = value.toString().replaceAll('"', r'\"');
|
||||
buffer.write(' -H "$key: $escapedValue"');
|
||||
});
|
||||
if (requestData != null) {
|
||||
String body;
|
||||
if (requestData is String) {
|
||||
body = requestData as String;
|
||||
} else {
|
||||
try {
|
||||
body = jsonEncode(requestData);
|
||||
} on Object catch (_) {
|
||||
body = requestData.toString();
|
||||
}
|
||||
}
|
||||
// 转义双引号和反斜杠
|
||||
final escapedBody = body.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
|
||||
buffer.write(' -d "$escapedBody"');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 创建副本并更新字段
|
||||
NetworkLogEntry copyWith({
|
||||
String? id,
|
||||
String? method,
|
||||
String? url,
|
||||
Map<String, dynamic>? headers,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
dynamic requestData,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
int? statusCode,
|
||||
|
|
@ -177,7 +146,6 @@ class NetworkLogEntry {
|
|||
method: method ?? this.method,
|
||||
url: url ?? this.url,
|
||||
headers: headers ?? this.headers,
|
||||
responseHeaders: responseHeaders ?? this.responseHeaders,
|
||||
requestData: requestData ?? this.requestData,
|
||||
queryParameters: queryParameters ?? this.queryParameters,
|
||||
statusCode: statusCode ?? this.statusCode,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:yx_net_inspector/src/widgets/panel/inspector_header.dart';
|
|||
import 'package:yx_net_inspector/src/widgets/panel/inspector_log_list.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_search_bar.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_statistics.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/panel/password_dialog.dart';
|
||||
|
||||
/// 网络检查器面板组件
|
||||
class YxInspectorPanel extends StatefulWidget {
|
||||
|
|
@ -143,16 +142,6 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
|||
return ListenableBuilder(
|
||||
listenable: widget.controller,
|
||||
builder: (context, child) {
|
||||
// 如果未解锁,显示密码解锁界面
|
||||
if (!widget.controller.isUnlocked) {
|
||||
return Center(
|
||||
child: PasswordDialog(
|
||||
theme: widget.theme,
|
||||
onUnlock: widget.controller.unlock,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget content;
|
||||
|
||||
if (_showDetailPage && _selectedLog != null) {
|
||||
|
|
@ -185,17 +174,6 @@ ${log.duration != null ? '- 结束时间: ${_formatDateTime(log.timestamp.add(lo
|
|||
isFullScreen: _isFullScreen,
|
||||
onToggleFullScreen: _toggleFullScreen,
|
||||
onClearLogs: widget.controller.clearLogs,
|
||||
onCopyLogs: () {
|
||||
final logs = widget.controller.getAllLogsAsString();
|
||||
Clipboard.setData(ClipboardData(text: logs));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Logs copied to clipboard'),
|
||||
duration: Duration(milliseconds: 1500),
|
||||
),
|
||||
);
|
||||
},
|
||||
onClose: widget.onClose,
|
||||
),
|
||||
InspectorStatistics(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
import 'package:yx_net_inspector/src/models/network_log_entry.dart';
|
||||
|
||||
|
|
@ -22,21 +21,10 @@ class YxLogDetailPage extends StatelessWidget {
|
|||
children: [
|
||||
_buildBasicInfoCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildCurlCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildRequestInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
if (log.headers != null && log.headers!.isNotEmpty) ...[
|
||||
_buildHeadersCard('请求头 (Request Headers)', log.headers!),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
_buildResponseInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
if (log.responseHeaders != null &&
|
||||
log.responseHeaders!.isNotEmpty) ...[
|
||||
_buildHeadersCard('响应头 (Response Headers)', log.responseHeaders!),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
if (log.errorMessage != null) ...[
|
||||
_buildErrorInfoCard(),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -399,123 +387,4 @@ class YxLogDetailPage extends StatelessWidget {
|
|||
'${dateTime.minute.toString().padLeft(2, '0')}:'
|
||||
'${dateTime.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Widget _buildHeadersCard(String title, Map<String, dynamic> headers) {
|
||||
return Card(
|
||||
color: theme.cardColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.list_alt, color: theme.primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.textColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(
|
||||
headers.entries.map((e) => '${e.key}: ${e.value}').join('\n'),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: theme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurlCard(BuildContext context) {
|
||||
final curlCommand = log.toCurlCommand();
|
||||
return Card(
|
||||
color: theme.cardColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.terminal, color: theme.warningColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'cURL 命令',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.textColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.copy, color: theme.primaryColor, size: 20),
|
||||
tooltip: '复制 cURL',
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: curlCommand));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('cURL 已复制到剪贴板'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxHeight: 150),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.warningColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableText(
|
||||
curlCommand,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
color: theme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ class InspectorDetailHeader extends StatelessWidget {
|
|||
|
||||
return isFullScreen
|
||||
? AppBar(
|
||||
title: const Text('请求详情'),
|
||||
backgroundColor: theme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
// excludeHeaderSemantics: false
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ class InspectorHeader extends StatelessWidget {
|
|||
required this.onToggleFullScreen,
|
||||
required this.onClearLogs,
|
||||
required this.onClose,
|
||||
this.onCopyLogs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -17,7 +16,6 @@ class InspectorHeader extends StatelessWidget {
|
|||
final VoidCallback onToggleFullScreen;
|
||||
final VoidCallback onClearLogs;
|
||||
final VoidCallback onClose;
|
||||
final VoidCallback? onCopyLogs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -35,12 +33,6 @@ class InspectorHeader extends StatelessWidget {
|
|||
icon: const Icon(Icons.clear_all, color: Colors.white),
|
||||
tooltip: '清空日志',
|
||||
),
|
||||
if (onCopyLogs != null)
|
||||
IconButton(
|
||||
onPressed: onCopyLogs,
|
||||
icon: const Icon(Icons.copy, color: Colors.white),
|
||||
tooltip: '复制所有日志',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: onClose,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
|
|
@ -68,6 +60,17 @@ class InspectorHeader extends StatelessWidget {
|
|||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.network_check, color: Colors.white, size: 24),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'网络检查器',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
...actions,
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,150 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
|
||||
/// 密码输入对话框
|
||||
class PasswordDialog extends StatefulWidget {
|
||||
const PasswordDialog({
|
||||
required this.onUnlock,
|
||||
required this.theme,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// 解锁回调
|
||||
final Future<bool> Function(String password) onUnlock;
|
||||
|
||||
/// 主题
|
||||
final YxNetInspectorTheme theme;
|
||||
|
||||
@override
|
||||
State<PasswordDialog> createState() => _PasswordDialogState();
|
||||
}
|
||||
|
||||
class _PasswordDialogState extends State<PasswordDialog> {
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool _obscureText = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleUnlock() async {
|
||||
final password = _passwordController.text;
|
||||
if (password.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final success = await widget.onUnlock(password);
|
||||
|
||||
if (mounted) {
|
||||
if (!success) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = '密码错误,请重试';
|
||||
});
|
||||
}
|
||||
// If success, the dialog (or lock screen) will typically be removed by the parent
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: widget.theme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.lock_outline,
|
||||
size: 48,
|
||||
color: widget.theme.primaryColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'安全访问',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: widget.theme.textColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'请输入密码以查看网络日志',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: widget.theme.secondaryTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscureText,
|
||||
decoration: InputDecoration(
|
||||
hintText: '输入密码',
|
||||
errorText: _errorMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
prefixIcon: const Icon(Icons.vpn_key),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureText ? Icons.visibility : Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscureText = !_obscureText;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _handleUnlock(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleUnlock,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: widget.theme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text('解锁'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
|||
|
||||
/// 包装你的应用并提供网络检查功能的主要组件
|
||||
class YxNetInspector extends StatefulWidget {
|
||||
|
||||
const YxNetInspector({
|
||||
required this.child,
|
||||
super.key,
|
||||
required this.child, super.key,
|
||||
this.config = const YxNetInspectorConfig(),
|
||||
this.theme = const YxNetInspectorTheme(),
|
||||
});
|
||||
|
|
@ -42,7 +42,6 @@ class YxNetInspector extends StatefulWidget {
|
|||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// 你的应用组件
|
||||
final Widget child;
|
||||
|
||||
|
|
@ -171,14 +170,12 @@ class YxNetInspectorGlobal {
|
|||
int? statusCode,
|
||||
dynamic responseData,
|
||||
Duration? duration,
|
||||
Map<String, dynamic>? responseHeaders,
|
||||
}) {
|
||||
controller.logResponse(
|
||||
id: id,
|
||||
statusCode: statusCode,
|
||||
responseData: responseData,
|
||||
duration: duration,
|
||||
responseHeaders: responseHeaders,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
// 核心导出
|
||||
export 'src/controller/yx_net_inspector_controller.dart';
|
||||
export 'src/interceptors/dio_interceptor.dart';
|
||||
export 'src/models/inspector_config.dart';
|
||||
export 'src/models/inspector_theme.dart';
|
||||
export 'src/models/network_log_entry.dart';
|
||||
|
|
|
|||
48
pubspec.lock
48
pubspec.lock
|
|
@ -41,22 +41,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.9.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -75,14 +59,6 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -131,14 +107,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -200,14 +168,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -232,14 +192,6 @@ packages:
|
|||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sdks:
|
||||
dart: ">=3.8.0-0 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name: yx_net_inspector
|
||||
description: A powerful network inspector with floating debug ball for Flutter apps. Monitor HTTP requests, responses, and debug network issues in real-time.
|
||||
version: 1.1.0
|
||||
version: 1.0.4
|
||||
homepage: https://github.com/your-username/yx_net_inspector
|
||||
|
||||
environment:
|
||||
|
|
@ -10,7 +10,6 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
dio: ^5.9.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||
import 'package:yx_net_inspector/src/interceptors/dio_interceptor.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||
|
||||
void main() {
|
||||
group('YxNetInspectorDioInterceptor Tests', () {
|
||||
late YxNetInspectorController controller;
|
||||
late Dio dio;
|
||||
|
||||
setUp(() {
|
||||
controller = YxNetInspectorController.instance;
|
||||
// Initialize controller with logging enabled (debug mode)
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
dio = Dio();
|
||||
// Add our inspector interceptor FIRST
|
||||
dio.interceptors.add(YxNetInspectorDioInterceptor());
|
||||
});
|
||||
|
||||
test('Should log request and response upon success', () async {
|
||||
final initialLogCount = controller.logs.length;
|
||||
|
||||
// Add a mock interceptor to return success response immediately
|
||||
dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
// Allow request to proceed (so our inspector sees it)
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (e, handler) => handler.next(e),
|
||||
onResponse: (e, handler) => handler.next(e),
|
||||
),
|
||||
);
|
||||
|
||||
// Mock Adapter to avoid real network
|
||||
dio.httpClientAdapter = _MockAdapter((options) {
|
||||
return ResponseBody.fromString(
|
||||
'{"message": "success"}',
|
||||
200,
|
||||
headers: {
|
||||
Headers.contentTypeHeader: [Headers.jsonContentType],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await dio.get<dynamic>('https://example.com/api/test');
|
||||
|
||||
expect(controller.logs.length, greaterThan(initialLogCount));
|
||||
final latestLog = controller.logs.first;
|
||||
|
||||
expect(latestLog.url, 'https://example.com/api/test');
|
||||
expect(latestLog.method, 'GET');
|
||||
expect(latestLog.statusCode, 200);
|
||||
});
|
||||
|
||||
test('Should log error upon failure', () async {
|
||||
final initialLogCount = controller.logs.length;
|
||||
|
||||
// Mock Adapter to throw error
|
||||
dio.httpClientAdapter = _MockAdapter((options) {
|
||||
throw DioException(
|
||||
requestOptions: options,
|
||||
error: 'Network Error',
|
||||
type: DioExceptionType.connectionError,
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await dio.post<dynamic>('https://example.com/api/fail',
|
||||
data: {'foo': 'bar'},);
|
||||
} on Object catch (_) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
expect(controller.logs.length, greaterThan(initialLogCount));
|
||||
final latestLog = controller.logs.first;
|
||||
|
||||
expect(latestLog.url, 'https://example.com/api/fail');
|
||||
expect(latestLog.method, 'POST');
|
||||
expect(latestLog.isSuccess, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _MockAdapter implements HttpClientAdapter {
|
||||
_MockAdapter(this._mockResponse);
|
||||
final ResponseBody Function(RequestOptions options) _mockResponse;
|
||||
|
||||
@override
|
||||
Future<ResponseBody> fetch(
|
||||
RequestOptions options,
|
||||
Stream<List<int>>? requestStream,
|
||||
Future<void>? cancelFuture,
|
||||
) async {
|
||||
return _mockResponse(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void close({bool force = false}) {}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart
|
|||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/panel/inspector_log_list.dart';
|
||||
|
||||
void main() {
|
||||
group('YxInspectorPanel Widget Tests', () {
|
||||
|
|
@ -35,25 +34,20 @@ void main() {
|
|||
),
|
||||
);
|
||||
|
||||
// 验证面板基本组件 (标题在非全屏模式已移除以节省空间)
|
||||
// Title removed from non-fullscreen mode to save space
|
||||
// 验证面板基本组件
|
||||
expect(find.text('网络检查器'), findsOneWidget);
|
||||
expect(find.byIcon(Icons.network_check), findsOneWidget);
|
||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('应该显示统计信息', (WidgetTester tester) async {
|
||||
// 添加一些测试数据
|
||||
controller.logRequest(
|
||||
id: 'test-1',
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
id: 'test-1', method: 'GET', url: 'https://example.com',);
|
||||
controller.logResponse(id: 'test-1', statusCode: 200);
|
||||
|
||||
controller.logRequest(
|
||||
id: 'test-2',
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
id: 'test-2', method: 'POST', url: 'https://example.com',);
|
||||
controller.logError(id: 'test-2', error: '网络错误');
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -71,15 +65,12 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 验证统计信息
|
||||
// 验证统计信息
|
||||
expect(find.text('总请求: 2'), findsOneWidget);
|
||||
expect(find.text('错误: 1'), findsOneWidget);
|
||||
|
||||
// Removed old fields
|
||||
// expect(find.text('总计'), findsOneWidget);
|
||||
// expect(find.text('成功'), findsOneWidget);
|
||||
// expect(find.text('失败'), findsOneWidget);
|
||||
// expect(find.text('成功率'), findsOneWidget);
|
||||
expect(find.text('总计'), findsOneWidget);
|
||||
expect(find.text('成功'), findsOneWidget);
|
||||
expect(find.text('失败'), findsOneWidget);
|
||||
expect(find.text('成功率'), findsOneWidget);
|
||||
expect(find.text('2'), findsOneWidget); // 总请求数
|
||||
expect(find.text('1'), findsWidgets); // 成功和失败各1个
|
||||
});
|
||||
|
||||
testWidgets('应该显示搜索栏', (WidgetTester tester) async {
|
||||
|
|
@ -98,24 +89,16 @@ void main() {
|
|||
// 验证搜索相关组件
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(find.text('搜索请求...'), findsOneWidget);
|
||||
expect(find.text('搜索请求...'), findsOneWidget);
|
||||
// FilterChip was removed/moved
|
||||
// expect(find.byType(FilterChip), findsOneWidget);
|
||||
// expect(find.text('仅显示错误'), findsOneWidget);
|
||||
expect(find.byType(FilterChip), findsOneWidget);
|
||||
expect(find.text('仅显示错误'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('搜索功能应该正常工作', (WidgetTester tester) async {
|
||||
// 添加测试数据
|
||||
controller.logRequest(
|
||||
id: 'user-1',
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
);
|
||||
id: 'user-1', method: 'GET', url: 'https://api.example.com/users',);
|
||||
controller.logRequest(
|
||||
id: 'post-1',
|
||||
method: 'POST',
|
||||
url: 'https://api.example.com/posts',
|
||||
);
|
||||
id: 'post-1', method: 'POST', url: 'https://api.example.com/posts',);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -142,17 +125,11 @@ void main() {
|
|||
testWidgets('错误过滤器应该正常工作', (WidgetTester tester) async {
|
||||
// 添加成功和失败的请求
|
||||
controller.logRequest(
|
||||
id: 'success',
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
id: 'success', method: 'GET', url: 'https://example.com',);
|
||||
controller.logResponse(id: 'success', statusCode: 200);
|
||||
|
||||
controller.logRequest(
|
||||
id: 'error',
|
||||
method: 'POST',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
id: 'error', method: 'POST', url: 'https://example.com',);
|
||||
controller.logError(id: 'error', error: '网络错误');
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
|
@ -169,21 +146,19 @@ void main() {
|
|||
|
||||
await tester.pump();
|
||||
|
||||
// 点击错误过滤器 (It's a TextButton now)
|
||||
await tester.tap(find.text('仅错误'));
|
||||
// 点击错误过滤器
|
||||
await tester.tap(find.byType(FilterChip));
|
||||
await tester.pump();
|
||||
|
||||
// 验证过滤器被激活 - Text changes to "显示全部"
|
||||
expect(find.text('显示全部'), findsOneWidget);
|
||||
// 验证过滤器被激活
|
||||
final filterChip = tester.widget<FilterChip>(find.byType(FilterChip));
|
||||
expect(filterChip.selected, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('清空日志按钮应该正常工作', (WidgetTester tester) async {
|
||||
// 添加一些日志
|
||||
controller.logRequest(
|
||||
id: 'test',
|
||||
method: 'GET',
|
||||
url: 'https://example.com',
|
||||
);
|
||||
id: 'test', method: 'GET', url: 'https://example.com',);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
|
@ -297,7 +272,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 验证日志条目显示
|
||||
expect(find.byType(InspectorLogItem), findsWidgets);
|
||||
expect(find.byType(Card), findsWidgets);
|
||||
expect(find.text('GET'), findsOneWidget);
|
||||
});
|
||||
|
||||
|
|
@ -325,7 +300,7 @@ void main() {
|
|||
await tester.pump();
|
||||
|
||||
// 点击日志条目
|
||||
await tester.tap(find.byType(InspectorLogItem).first);
|
||||
await tester.tap(find.byType(InkWell).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 验证导航到详情页(这里主要验证没有报错)
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yx_net_inspector/src/controller/yx_net_inspector_controller.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_config.dart';
|
||||
import 'package:yx_net_inspector/src/models/inspector_theme.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/inspector_panel.dart';
|
||||
import 'package:yx_net_inspector/src/widgets/panel/password_dialog.dart';
|
||||
|
||||
void main() {
|
||||
group('Password Protection Tests', () {
|
||||
late YxNetInspectorController controller;
|
||||
const theme = YxNetInspectorTheme();
|
||||
|
||||
setUp(() {
|
||||
controller = YxNetInspectorController.instance;
|
||||
});
|
||||
|
||||
testWidgets('Panel should be unlocked by default (no password)',
|
||||
(WidgetTester tester) async {
|
||||
controller.initialize(const YxNetInspectorConfig());
|
||||
|
||||
expect(controller.isUnlocked, isTrue);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YxInspectorPanel(
|
||||
theme: theme,
|
||||
controller: controller,
|
||||
onClose: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(PasswordDialog), findsNothing);
|
||||
expect(find.byIcon(Icons.close), findsOneWidget); // Normal panel content
|
||||
});
|
||||
|
||||
testWidgets('Panel should be locked if password is set',
|
||||
(WidgetTester tester) async {
|
||||
controller.initialize(const YxNetInspectorConfig(
|
||||
password: '123',
|
||||
),);
|
||||
|
||||
expect(controller.isUnlocked, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YxInspectorPanel(
|
||||
theme: theme,
|
||||
controller: controller,
|
||||
onClose: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(PasswordDialog), findsOneWidget);
|
||||
expect(find.text('安全访问'), findsOneWidget);
|
||||
expect(
|
||||
find.byIcon(Icons.close), findsNothing,); // Should hide panel content
|
||||
});
|
||||
|
||||
testWidgets('Entering correct password should unlock panel',
|
||||
(WidgetTester tester) async {
|
||||
controller.initialize(const YxNetInspectorConfig(
|
||||
password: '123',
|
||||
),);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YxInspectorPanel(
|
||||
theme: theme,
|
||||
controller: controller,
|
||||
onClose: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Enter correct password
|
||||
await tester.enterText(find.byType(TextField), '123');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump(); // Start async unlock
|
||||
await tester.pump(); // Rebuild with unlocked state
|
||||
|
||||
expect(controller.isUnlocked, isTrue);
|
||||
expect(find.byType(PasswordDialog), findsNothing);
|
||||
expect(find.byIcon(Icons.close), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Entering wrong password should show error',
|
||||
(WidgetTester tester) async {
|
||||
controller.initialize(const YxNetInspectorConfig(
|
||||
password: '123',
|
||||
),);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: YxInspectorPanel(
|
||||
theme: theme,
|
||||
controller: controller,
|
||||
onClose: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Enter wrong password
|
||||
await tester.enterText(find.byType(TextField), '000');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump(); // Start async unlock
|
||||
await tester.pump(); // Rebuild
|
||||
|
||||
expect(controller.isUnlocked, isFalse);
|
||||
expect(find.byType(PasswordDialog), findsOneWidget);
|
||||
expect(find.text('密码错误,请重试'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue