Files
Navius/frontend/test/features/home/presentation/home_page_test.dart
T
CNWei e2cc3ac30f feat: 进行功能分区方便后期维护
- 添加导航条目和分类数据模型
- 实现导航卡片网格展示
- 支持点击卡片打开链接
- 添加主题样式(渐变色、卡片阴影)
- 区分系统默认和自定义条目
- 添加分类统计信息"
2026-09-13 22:49:02 +08:00

323 lines
11 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_test/flutter_test.dart';
import 'package:frontend/core/constants/app_sizes.dart';
import 'package:frontend/core/theme/app_colors.dart';
import 'package:frontend/features/home/data/mock_home_repository.dart';
import 'package:frontend/features/home/presentation/pages/home_page.dart';
import 'package:frontend/features/home/presentation/widgets/link_card.dart';
import 'package:frontend/features/home/presentation/widgets/link_section_view.dart';
import 'package:frontend/features/home/presentation/widgets/sidebar_nav.dart';
import '../../../helpers/fake_url_opener.dart';
void main() {
late FakeUrlOpener urlOpener;
setUp(() {
urlOpener = FakeUrlOpener();
});
Future<void> pumpPage(
WidgetTester tester, {
// 高度取 420:原型只有两段内容,视口再高就滚不动了,
// 「回到顶部」这类用例也就没有意义。
Size size = const Size(900, 420),
}) async {
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.pumpWidget(
MaterialApp(
home: HomePage(
repository: const MockHomeRepository(),
urlOpener: urlOpener,
),
),
);
await tester.pump();
}
/// 侧栏里的导航文字(避免和内容区的分区标题同名而混淆)。
Finder navText(String label) {
return find.descendant(
of: find.byType(SidebarNav),
matching: find.text(label),
);
}
/// 内容区的分区标题。
Finder sectionTitle(String label) {
return find.descendant(
of: find.byType(LinkSectionView),
matching: find.text(label),
);
}
double sidebarWidth(WidgetTester tester) {
return tester.getSize(find.byType(SidebarNav)).width;
}
Color? navColor(WidgetTester tester, String label) {
return tester.widget<Text>(navText(label)).style?.color;
}
/// 一级条目当前的底色(透明表示没有高亮)。
List<String> highlightedRows(WidgetTester tester) {
final List<String> rows = <String>[];
for (final String label in <String>['常用推荐', '素材资源', '开发工具']) {
final AnimatedContainer container = tester.widget<AnimatedContainer>(
find
.ancestor(
of: navText(label),
matching: find.byType(AnimatedContainer),
)
.first,
);
final Color? color = (container.decoration! as BoxDecoration).color;
if (color != null && color.toARGB32() != 0) {
rows.add(label);
}
}
return rows;
}
/// 主内容区的滚动控制器。
///
/// 侧栏的导航容器也是一个 SingleChildScrollView,所以这里用
/// 「搜索框的祖先」来精确定位主滚动视图,而不是靠 `.first` / `.last`。
ScrollController mainController(WidgetTester tester) {
final Finder mainScroll = find.ancestor(
of: find.byType(TextField),
matching: find.byType(SingleChildScrollView),
);
return tester.widget<SingleChildScrollView>(mainScroll).controller!;
}
double scrollOffset(WidgetTester tester) => mainController(tester).offset;
Future<void> expandMaterials(WidgetTester tester) async {
await tester.tap(navText('素材资源'));
await tester.pumpAndSettle();
}
/// 折叠态下导航项是空行(没有文字也没有图标),只能按位置定位:
/// 0 是侧栏本身,1~3 是三条一级导航。
Finder collapsedRow(int index) {
return find
.descendant(
of: find.byType(SidebarNav),
matching: find.byType(AnimatedContainer),
)
.at(index);
}
group('布局', () {
testWidgets('侧栏展开常驻,渲染 logo、三条导航与底部提交入口', (WidgetTester tester) async {
await pumpPage(tester);
expect(find.byType(SidebarNav), findsOneWidget);
expect(sidebarWidth(tester), AppSizes.sidebarWidth);
expect(find.text('Suxing Nav'), findsOneWidget);
expect(navText('常用推荐'), findsOneWidget);
expect(navText('素材资源'), findsOneWidget);
expect(navText('开发工具'), findsOneWidget);
expect(navText('提交网站'), findsOneWidget);
expect(find.byType(AppBar), findsNothing);
});
testWidgets('子菜单默认收起', (WidgetTester tester) async {
await pumpPage(tester);
expect(navText('UI 设计'), findsNothing);
expect(navText('图标库'), findsNothing);
expect(navText('前端框架'), findsNothing);
});
testWidgets('两个分区与五张卡片,与原型一致', (WidgetTester tester) async {
await pumpPage(tester);
expect(find.byType(LinkSectionView), findsNWidgets(2));
expect(find.byType(LinkCard), findsNWidgets(5));
expect(sectionTitle('常用推荐'), findsOneWidget);
expect(sectionTitle('素材资源 - UI 设计'), findsOneWidget);
});
});
group('侧栏折叠(对应 #toggleBtn)', () {
testWidgets('点折叠按钮后宽度变为 64,logo 变成首字', (WidgetTester tester) async {
await pumpPage(tester);
await tester.tap(find.byTooltip('展开/折叠侧边栏'));
await tester.pumpAndSettle();
expect(sidebarWidth(tester), AppSizes.sidebarCollapsedWidth);
expect(find.text('Suxing Nav'), findsNothing);
expect(find.text('S'), findsOneWidget);
// 折叠态下 .nav-text 与 .submit-text 都被隐藏,只剩空行。
expect(navText('常用推荐'), findsNothing);
expect(navText('提交网站'), findsNothing);
});
testWidgets('再点一次可以展开回来', (WidgetTester tester) async {
await pumpPage(tester);
await tester.tap(find.byTooltip('展开/折叠侧边栏'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('展开/折叠侧边栏'));
await tester.pumpAndSettle();
expect(sidebarWidth(tester), AppSizes.sidebarWidth);
expect(find.text('Suxing Nav'), findsOneWidget);
});
});
group('分组子菜单(对应 .has-sub 的脚本)', () {
testWidgets('点击分组展开子菜单,再点收起', (WidgetTester tester) async {
await pumpPage(tester);
await expandMaterials(tester);
expect(navText('UI 设计'), findsOneWidget);
expect(navText('图标库'), findsOneWidget);
expect(navText('高清壁纸'), findsOneWidget);
await tester.tap(navText('素材资源'));
await tester.pumpAndSettle();
expect(navText('UI 设计'), findsNothing);
});
testWidgets('折叠状态下点分组会先展开侧栏', (WidgetTester tester) async {
await pumpPage(tester);
await tester.tap(find.byTooltip('展开/折叠侧边栏'));
await tester.pumpAndSettle();
expect(sidebarWidth(tester), AppSizes.sidebarCollapsedWidth);
await tester.tap(collapsedRow(2));
await tester.pumpAndSettle();
expect(sidebarWidth(tester), AppSizes.sidebarWidth);
expect(navText('UI 设计'), findsOneWidget);
});
});
group('导航与内容', () {
testWidgets('高亮固定写在第一个条目上', (WidgetTester tester) async {
await pumpPage(tester);
expect(navColor(tester, '常用推荐'), AppColors.primary);
expect(navColor(tester, '素材资源'), AppColors.textSecondary);
expect(highlightedRows(tester), <String>['常用推荐']);
});
testWidgets('点有对应分区的导航会滚过去,高亮不变', (WidgetTester tester) async {
await pumpPage(tester);
final double before = scrollOffset(tester);
await expandMaterials(tester);
await tester.tap(navText('UI 设计'));
await tester.pumpAndSettle();
expect(scrollOffset(tester), greaterThan(before));
expect(navColor(tester, '常用推荐'), AppColors.primary);
expect(highlightedRows(tester), <String>['常用推荐']);
});
testWidgets('点没有对应分区的导航不会有任何反应', (WidgetTester tester) async {
await pumpPage(tester);
await expandMaterials(tester);
await tester.tap(navText('UI 设计'));
await tester.pumpAndSettle();
final double afterScroll = scrollOffset(tester);
// #icon 在原型里没有对应元素。
await tester.tap(navText('图标库'));
await tester.pumpAndSettle();
expect(scrollOffset(tester), afterScroll);
});
testWidgets('搜索框只接收输入,不过滤内容', (WidgetTester tester) async {
await pumpPage(tester);
await tester.enterText(find.byType(TextField), 'GitHub');
await tester.pumpAndSettle();
expect(find.byType(LinkSectionView), findsNWidgets(2));
expect(find.byType(LinkCard), findsNWidgets(5));
expect(
tester.widget<TextField>(find.byType(TextField)).controller?.text,
'GitHub',
);
});
});
group('滚动', () {
testWidgets('滚动主内容不会重建侧栏,也不会改动高亮', (WidgetTester tester) async {
await pumpPage(tester);
final ScrollController controller = mainController(tester);
AnimatedContainer firstRow() {
return tester.widget<AnimatedContainer>(
find
.ancestor(
of: navText('常用推荐'),
matching: find.byType(AnimatedContainer),
)
.first,
);
}
final AnimatedContainer beforeScroll = firstRow();
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(
identical(beforeScroll, firstRow()),
isTrue,
reason: '滚动主内容时侧栏被重建了',
);
expect(highlightedRows(tester), <String>['常用推荐']);
});
testWidgets('回到顶部把偏移归零', (WidgetTester tester) async {
await pumpPage(tester);
final ScrollController controller = mainController(tester);
controller.jumpTo(controller.position.maxScrollExtent);
await tester.pump();
expect(scrollOffset(tester), greaterThan(0));
await tester.tap(find.byTooltip('回到顶部'));
await tester.pumpAndSettle();
expect(scrollOffset(tester), 0);
});
});
group('其它', () {
testWidgets('点击卡片交由 UrlOpener 打开对应地址', (WidgetTester tester) async {
await pumpPage(tester);
await tester.tap(find.text('GitHub'));
await tester.pump();
expect(urlOpener.openedUrls, <String>['https://github.com']);
});
testWidgets('点提交网站不会有任何反应', (WidgetTester tester) async {
await pumpPage(tester);
await tester.tap(navText('提交网站'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 400));
expect(find.byType(SnackBar), findsNothing);
});
});
}