8539f05a66
- 新增录音启动延迟处理,解决权限未就绪时的竞态问题 - 实现历史分页滚动位置保持,提升加载体验 - 添加文本输入框点击键盘显示与焦点管理 - 优化 ChatBloc provider 到 MultiBlocProvider 支持 - 修复 ApiException 429 错误详情解析(支持 JSON 字符串 body) - 改进 LocalNotificationService 精确闹钟权限请求 - 优化 UiSchemaRenderer GridView children 生成 - 支持导航 action 的 replace 参数 - 移除 Agent router 速率限制逻辑(_allow_run_request, _allow_transcribe_request) - 补充相关单元测试与集成测试
278 lines
8.0 KiB
Dart
278 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:social_app/features/chat/ui/widgets/ui_schema_renderer.dart';
|
|
|
|
void main() {
|
|
group('UiSchemaRenderer', () {
|
|
testWidgets('renders stack title and badge', (tester) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'locale': 'zh-CN',
|
|
'status': 'success',
|
|
'theme': 'default',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'card',
|
|
'children': [
|
|
{'type': 'text', 'role': 'title', 'content': '日程已创建'},
|
|
{'type': 'badge', 'label': 'SUCCESS', 'status': 'success'},
|
|
],
|
|
},
|
|
};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
);
|
|
|
|
expect(find.text('日程已创建'), findsOneWidget);
|
|
expect(find.text('SUCCESS'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders kv node values', (tester) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'card',
|
|
'children': [
|
|
{
|
|
'type': 'kv',
|
|
'items': [
|
|
{'key': 'title', 'label': '标题', 'value': '评审会'},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
);
|
|
|
|
expect(find.text('标题'), findsOneWidget);
|
|
expect(find.text('评审会'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders batch result list items in one card', (tester) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'card',
|
|
'status': 'warning',
|
|
'children': [
|
|
{'type': 'text', 'role': 'title', 'content': '日历操作完成'},
|
|
{
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'gap': 8,
|
|
'children': [
|
|
{
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'card',
|
|
'children': [
|
|
{'type': 'text', 'role': 'body', 'content': '#1 create'},
|
|
{'type': 'text', 'role': 'caption', 'content': '成功'},
|
|
{'type': 'text', 'role': 'caption', 'content': '日程「晨会」已创建'},
|
|
],
|
|
},
|
|
{
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'card',
|
|
'children': [
|
|
{'type': 'text', 'role': 'body', 'content': '#2 delete'},
|
|
{'type': 'text', 'role': 'caption', 'content': '失败'},
|
|
{
|
|
'type': 'text',
|
|
'role': 'caption',
|
|
'content': 'Schedule item not found',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
);
|
|
|
|
expect(find.text('日历操作完成'), findsOneWidget);
|
|
expect(find.text('#1 create'), findsOneWidget);
|
|
expect(find.text('#2 delete'), findsOneWidget);
|
|
expect(find.text('成功'), findsOneWidget);
|
|
expect(find.text('失败'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders fallback for invalid schema', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: UiSchemaRenderer.renderSchema({'version': '2.0'}),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.textContaining('无效 UI Schema'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('handles navigation action by pushing target page', (
|
|
tester,
|
|
) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'plain',
|
|
'children': [
|
|
{
|
|
'type': 'button',
|
|
'label': '查看待办',
|
|
'style': 'primary',
|
|
'action': {
|
|
'type': 'navigation',
|
|
'path': '/todo/123',
|
|
'params': {'from': 'assistant'},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
final router = GoRouter(
|
|
initialLocation: '/',
|
|
routes: [
|
|
GoRoute(
|
|
path: '/',
|
|
builder: (context, state) =>
|
|
Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
GoRoute(
|
|
path: '/todo/:id',
|
|
builder: (context, state) => Text(
|
|
'todo detail ${state.pathParameters['id']} from ${state.uri.queryParameters['from']}',
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
|
await tester.tap(find.text('查看待办'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('todo detail 123 from assistant'), findsOneWidget);
|
|
expect(router.canPop(), isTrue);
|
|
|
|
router.pop();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('查看待办'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('uses replace navigation when replace is true', (tester) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'plain',
|
|
'children': [
|
|
{
|
|
'type': 'button',
|
|
'label': '替换跳转',
|
|
'style': 'primary',
|
|
'action': {
|
|
'type': 'navigation',
|
|
'path': '/todo/456',
|
|
'replace': true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
final router = GoRouter(
|
|
initialLocation: '/',
|
|
routes: [
|
|
GoRoute(
|
|
path: '/',
|
|
builder: (context, state) =>
|
|
Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
GoRoute(
|
|
path: '/todo/:id',
|
|
builder: (context, state) =>
|
|
Text('todo detail ${state.pathParameters['id']}'),
|
|
),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
|
await tester.tap(find.text('替换跳转'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('todo detail 456'), findsOneWidget);
|
|
expect(router.canPop(), isFalse);
|
|
|
|
expect(find.text('todo detail 456'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('does not navigate for placeholder path', (tester) async {
|
|
final schema = {
|
|
'version': '2.0',
|
|
'root': {
|
|
'type': 'stack',
|
|
'direction': 'vertical',
|
|
'appearance': 'plain',
|
|
'children': [
|
|
{
|
|
'type': 'button',
|
|
'label': '坏路径',
|
|
'style': 'primary',
|
|
'action': {'type': 'navigation', 'path': '/todo/:id'},
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
final router = GoRouter(
|
|
initialLocation: '/',
|
|
routes: [
|
|
GoRoute(
|
|
path: '/',
|
|
builder: (context, state) =>
|
|
Scaffold(body: UiSchemaRenderer.renderSchema(schema)),
|
|
),
|
|
GoRoute(
|
|
path: '/todo/:id',
|
|
builder: (context, state) => const Text('detail'),
|
|
),
|
|
],
|
|
);
|
|
|
|
await tester.pumpWidget(MaterialApp.router(routerConfig: router));
|
|
await tester.tap(find.text('坏路径'));
|
|
await tester.pumpAndSettle();
|
|
await tester.pump(const Duration(seconds: 3));
|
|
|
|
expect(find.text('坏路径'), findsOneWidget);
|
|
expect(find.text('detail'), findsNothing);
|
|
});
|
|
});
|
|
}
|