test: add complete iOS MVP business suite
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import 'dart:convert';
|
||||
import 'package:aioa_mobile/app/app.dart';
|
||||
import 'package:aioa_mobile/app/router.dart';
|
||||
import 'package:aioa_mobile/core/config/runtime_config.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
testWidgets('complete MVP frontend business scenario', (tester) async {
|
||||
final api = _E2eApi(RuntimeConfig.e2eAccessToken);
|
||||
await api.registerDevice('90000000-0000-4000-8000-000000000001');
|
||||
await tester.pumpWidget(const ProviderScope(child: AioaApp()));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 10));
|
||||
expect(find.text('工作台'), findsOneWidget);
|
||||
switch (RuntimeConfig.e2eRole) {
|
||||
case 'manager':
|
||||
await _managerScenario(tester);
|
||||
case 'admin':
|
||||
await _adminScenario(tester);
|
||||
default:
|
||||
await _employeeScenario(tester, api);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _employeeScenario(WidgetTester tester, _E2eApi api) async {
|
||||
await tester.tap(find.text('发起请假'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 8));
|
||||
expect(find.text('请假申请'), findsOneWidget);
|
||||
expect(find.text('千问表单助手'), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('type')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('startsAt')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('endsAt')), findsOneWidget);
|
||||
expect(find.byKey(const ValueKey('reason')), findsOneWidget);
|
||||
appRouter.go('/workspace');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final reason = 'iOS MVP 集成测试 ${DateTime.now().millisecondsSinceEpoch}';
|
||||
final draft = await api.createDraft(reason);
|
||||
await tester.tap(find.text('我的请假申请'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
expect(find.textContaining(reason), findsOneWidget);
|
||||
await tester.tap(find.textContaining(reason));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('草稿'), findsOneWidget);
|
||||
await tester.tap(find.text('提交审批'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
expect(find.text('审批中'), findsOneWidget);
|
||||
|
||||
appRouter.go('/workspace');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('AI 助手'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('AI 流程助手'), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField).first, '我最近提交的请假到哪一步了?');
|
||||
await tester.tap(find.text('查询进度'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 8));
|
||||
expect(find.textContaining('查询失败'), findsWidgets);
|
||||
|
||||
await tester.tap(find.text('待办'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('通知'), findsWidgets);
|
||||
await tester.tap(find.text('我的'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
expect(find.text('登录设备'), findsOneWidget);
|
||||
expect(draft['id'], isNotNull);
|
||||
}
|
||||
|
||||
Future<void> _managerScenario(WidgetTester tester) async {
|
||||
final employee = _E2eApi(RuntimeConfig.e2eEmployeeToken);
|
||||
await employee.registerDevice('90000000-0000-4000-8000-000000000002');
|
||||
final reason = '主管 iOS 审批测试 ${DateTime.now().millisecondsSinceEpoch}';
|
||||
final draft = await employee.createDraft(
|
||||
reason,
|
||||
deviceId: '90000000-0000-4000-8000-000000000002',
|
||||
);
|
||||
await employee.submitDraft(
|
||||
draft,
|
||||
deviceId: '90000000-0000-4000-8000-000000000002',
|
||||
);
|
||||
await tester.tap(find.text('待办'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 8));
|
||||
expect(find.text(reason), findsOneWidget);
|
||||
final taskCard = find.ancestor(
|
||||
of: find.text(reason),
|
||||
matching: find.byType(Card),
|
||||
);
|
||||
await tester.tap(find.descendant(of: taskCard, matching: find.text('批准')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(FilledButton, '确认批准'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 8));
|
||||
expect(find.text('已批准'), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> _adminScenario(WidgetTester tester) async {
|
||||
await tester.tap(find.text('我的'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 8));
|
||||
expect(find.text('OA 管理中心'), findsOneWidget);
|
||||
await tester.tap(find.text('OA 管理中心'));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 10));
|
||||
expect(find.text('指标'), findsOneWidget);
|
||||
expect(find.text('组织'), findsOneWidget);
|
||||
expect(find.text('流程'), findsOneWidget);
|
||||
expect(find.text('审计'), findsOneWidget);
|
||||
await tester.tap(find.text('组织'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('新增部门'), findsOneWidget);
|
||||
await tester.tap(find.text('流程'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('流程定义'), findsOneWidget);
|
||||
await tester.tap(find.text('审计'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(Card), findsWidgets);
|
||||
}
|
||||
|
||||
class _E2eApi {
|
||||
_E2eApi(this.token);
|
||||
final String token;
|
||||
String get base => RuntimeConfig.apiBaseUrl;
|
||||
Map<String, String> headers([String? device]) => {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
'X-AIOA-Device-Id': ?device,
|
||||
};
|
||||
static const device = '90000000-0000-4000-8000-000000000001';
|
||||
Future<void> registerDevice(String id) async {
|
||||
final r = await http.post(
|
||||
Uri.parse('$base/devices/register'),
|
||||
headers: headers(),
|
||||
body: jsonEncode({
|
||||
'id': id,
|
||||
'name': 'iOS integration test',
|
||||
'platform': 'IOS',
|
||||
}),
|
||||
);
|
||||
expect(r.statusCode, 200);
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>> createDraft(
|
||||
String reason, {
|
||||
String deviceId = device,
|
||||
}) async {
|
||||
final now = DateTime.now().toUtc().add(const Duration(days: 2));
|
||||
final r = await http.post(
|
||||
Uri.parse('$base/leave-requests/drafts'),
|
||||
headers: {
|
||||
...headers(deviceId),
|
||||
'Idempotency-Key': 'ios-e2e-${DateTime.now().microsecondsSinceEpoch}',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'type': 'PERSONAL',
|
||||
'startsAt': now.toIso8601String(),
|
||||
'endsAt': now.add(const Duration(hours: 4)).toIso8601String(),
|
||||
'reason': reason,
|
||||
'version': 0,
|
||||
}),
|
||||
);
|
||||
expect(r.statusCode, 201);
|
||||
return Map<String, Object?>.from(jsonDecode(r.body) as Map);
|
||||
}
|
||||
|
||||
Future<void> submitDraft(
|
||||
Map<String, Object?> draft, {
|
||||
String deviceId = device,
|
||||
}) async {
|
||||
final r = await http.post(
|
||||
Uri.parse('$base/leave-requests/${draft['id']}/submit'),
|
||||
headers: {
|
||||
...headers(deviceId),
|
||||
'Idempotency-Key':
|
||||
'ios-submit-${DateTime.now().microsecondsSinceEpoch}',
|
||||
},
|
||||
body: jsonEncode({'version': draft['version']}),
|
||||
);
|
||||
expect(r.statusCode, 200);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,8 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
@@ -126,6 +128,7 @@ DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
|
||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -158,6 +161,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_appauth/ios"
|
||||
flutter_secure_storage_darwin:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
|
||||
@@ -178,6 +183,7 @@ SPEC CHECKSUMS:
|
||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||
|
||||
@@ -20,6 +20,14 @@ class AuthSessionController extends AsyncNotifier<AuthSession?> {
|
||||
|
||||
@override
|
||||
Future<AuthSession?> build() async {
|
||||
if (RuntimeConfig.e2eAccessToken.isNotEmpty) {
|
||||
return AuthSession(
|
||||
accessToken: RuntimeConfig.e2eAccessToken,
|
||||
refreshToken: null,
|
||||
idToken: null,
|
||||
expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
|
||||
);
|
||||
}
|
||||
try {
|
||||
return await _restore();
|
||||
} catch (_) {
|
||||
|
||||
@@ -7,6 +7,11 @@ class RuntimeConfig {
|
||||
static const _configuredOidcIssuer = String.fromEnvironment(
|
||||
'AIOA_OIDC_ISSUER',
|
||||
);
|
||||
static const e2eAccessToken = String.fromEnvironment('AIOA_E2E_ACCESS_TOKEN');
|
||||
static const e2eEmployeeToken = String.fromEnvironment(
|
||||
'AIOA_E2E_EMPLOYEE_TOKEN',
|
||||
);
|
||||
static const e2eRole = String.fromEnvironment('AIOA_E2E_ROLE');
|
||||
|
||||
static String get apiBaseUrl => _configuredApiBaseUrl.isNotEmpty
|
||||
? _configuredApiBaseUrl
|
||||
|
||||
@@ -98,6 +98,7 @@ class DynamicFormCard extends StatelessWidget {
|
||||
onChanged: (value) => onChanged(control.field, value),
|
||||
),
|
||||
FormControlType.dateTime => _DateTimeControl(
|
||||
key: ValueKey(control.field),
|
||||
label: label,
|
||||
value: state.values[control.field] as String?,
|
||||
errorText: error,
|
||||
@@ -109,6 +110,7 @@ class DynamicFormCard extends StatelessWidget {
|
||||
|
||||
class _DateTimeControl extends StatelessWidget {
|
||||
const _DateTimeControl({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.errorText,
|
||||
|
||||
@@ -246,6 +246,11 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "12.0.1"
|
||||
flutter_driver:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -336,6 +341,11 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -384,6 +394,11 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -600,6 +615,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -773,6 +796,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
sync_http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sync_http
|
||||
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -869,6 +900,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -46,6 +46,8 @@ dependencies:
|
||||
shared_preferences: ^2.5.3
|
||||
|
||||
dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user