test: add complete iOS MVP business suite
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
# iOS MVP 前端业务测试方案
|
||||||
|
|
||||||
|
## 测试目标
|
||||||
|
|
||||||
|
在真实 iOS Simulator、Keycloak、PostgreSQL、Flowable 和 Kotlin 后端上验证 Flutter MVP 业务闭环,而不是只验证孤立 Widget。
|
||||||
|
|
||||||
|
## 自动化场景
|
||||||
|
|
||||||
|
### 员工
|
||||||
|
|
||||||
|
- 使用临时测试 Token 进入工作台,生产 OIDC 流程不被替换。
|
||||||
|
- 打开 Schema 动态请假表单,检查类型、开始时间、结束时间、原因及千问表单助手。
|
||||||
|
- 通过真实 API 创建唯一草稿,在“我的申请”中找到并打开。
|
||||||
|
- 从申请详情确认提交,验证状态进入“审批中”。
|
||||||
|
- 查询 AI 流程进度;千问未配置时验证明确的失败降级。
|
||||||
|
- 检查待办/通知入口与个人中心设备列表。
|
||||||
|
|
||||||
|
附件选择器和系统相册属于 iOS 系统 UI,仓库中的附件 Repository、上传进度、类型和幂等测试负责自动化覆盖;真实文件选择纳入人工验收步骤。
|
||||||
|
|
||||||
|
### 部门主管
|
||||||
|
|
||||||
|
- 测试自动用员工身份创建并提交一条唯一申请。
|
||||||
|
- 主管进入待办,只定位该申请并批准。
|
||||||
|
- 验证批准成功提示,避免依赖历史测试数据。
|
||||||
|
|
||||||
|
### OA 管理员
|
||||||
|
|
||||||
|
- 验证权限驱动的“OA 管理中心”入口。
|
||||||
|
- 验证业务指标、组织用户与角色、流程定义/实例、脱敏审计四个页面。
|
||||||
|
|
||||||
|
## 执行前提
|
||||||
|
|
||||||
|
- PostgreSQL 与 Keycloak 容器健康。
|
||||||
|
- 后端运行在 `http://localhost:8080`。
|
||||||
|
- 已启动一个 iOS Simulator。
|
||||||
|
- Realm 中仅限本地开发的 employee、manager、admin 用户可用。
|
||||||
|
|
||||||
|
## 执行命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/test-ios-mvp.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本不会输出或持久化访问 Token。`AIOA_E2E_ACCESS_TOKEN` 只在显式测试构建中生效,普通 Debug、Release 和商店构建仍使用 OIDC Authorization Code + PKCE。
|
||||||
|
|
||||||
|
## 人工补充验收
|
||||||
|
|
||||||
|
1. 点击 Keycloak 登录并完成浏览器回调。
|
||||||
|
2. 从系统文件选择器选择 JPEG、PNG 和 PDF,确认上传进度和附件状态。
|
||||||
|
3. 在配置 Firebase 的真机上确认前台、后台和点击跳转通知。
|
||||||
|
4. 在配置千问密钥的环境确认 AI 草稿建议成功,并检查确认卡片不会自动提交。
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# iOS MVP 前端业务测试报告
|
||||||
|
|
||||||
|
- 执行日期:2026-07-18
|
||||||
|
- 设备:iPhone 17 Pro Simulator
|
||||||
|
- 系统:iOS 26.5
|
||||||
|
- Flutter:本机 stable
|
||||||
|
- 后端:本地 Kotlin/Spring Boot
|
||||||
|
- 基础服务:PostgreSQL 17.5、Keycloak 26.2.5、Flowable 7.2
|
||||||
|
|
||||||
|
## 自动化结果
|
||||||
|
|
||||||
|
| 场景 | 结果 | 主要验证点 |
|
||||||
|
|---|---|---|
|
||||||
|
| 员工 | 通过 | 工作台、Schema 表单、千问入口、真实草稿、我的申请、提交审批、流程状态、AI 降级、通知、设备 |
|
||||||
|
| 部门主管 | 通过 | 自动构造唯一待办、列表定位、批准、成功反馈 |
|
||||||
|
| OA 管理员 | 通过 | 权限驱动入口、指标、组织角色、流程定义/实例、脱敏审计 |
|
||||||
|
|
||||||
|
最终脚本输出:`All iOS MVP frontend business scenarios passed.`
|
||||||
|
|
||||||
|
同时通过:
|
||||||
|
|
||||||
|
- Flutter Analyze
|
||||||
|
- 27 项 Flutter 单元与 Widget 测试
|
||||||
|
- iOS Simulator 编译、安装和进程启动
|
||||||
|
- 登录取消与安全存储异常恢复验收
|
||||||
|
|
||||||
|
## 测试期间发现并处理
|
||||||
|
|
||||||
|
1. Keycloak 登录取消曾直接显示底层 AppAuth 异常,已改为静默取消或简短中文错误。
|
||||||
|
2. 模拟器重装后安全存储异常曾阻塞登录页,已降级为无本地会话。
|
||||||
|
3. 通知标签包含动态未读数,集成测试改为语义匹配。
|
||||||
|
4. 管理员 Token 因整套测试耗时接近过期,脚本改为每个角色执行前即时获取 Token。
|
||||||
|
5. 主管场景原先可能命中历史待办,现改为自动创建并只处理唯一测试申请。
|
||||||
|
|
||||||
|
## 外部配置相关验证
|
||||||
|
|
||||||
|
以下能力的代码路径、失败降级和 Repository 已自动化覆盖,但成功投递依赖外部正式凭证或系统 UI:
|
||||||
|
|
||||||
|
- 千问真实回答:需要有效且已轮换的 `QWEN_API_KEY`。
|
||||||
|
- APNs/FCM 真机通知:需要 Firebase 项目与 Apple 推送配置。
|
||||||
|
- 系统文件选择器:需人工选择 JPEG、PNG、PDF 完成真机上传验收。
|
||||||
|
|
||||||
|
这些外部条件不影响当前 MVP 前端业务闭环、权限边界和失败处理的自动化通过结果。
|
||||||
@@ -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/UserDefaults (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
|
- integration_test (0.0.1):
|
||||||
|
- Flutter
|
||||||
- nanopb (3.30910.0):
|
- nanopb (3.30910.0):
|
||||||
- nanopb/decode (= 3.30910.0)
|
- nanopb/decode (= 3.30910.0)
|
||||||
- nanopb/encode (= 3.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
@@ -126,6 +128,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
|
- flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`)
|
||||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
- 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`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -158,6 +161,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_appauth/ios"
|
:path: ".symlinks/plugins/flutter_appauth/ios"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
|
|
||||||
@@ -178,6 +183,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
|
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ class AuthSessionController extends AsyncNotifier<AuthSession?> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<AuthSession?> build() async {
|
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 {
|
try {
|
||||||
return await _restore();
|
return await _restore();
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ class RuntimeConfig {
|
|||||||
static const _configuredOidcIssuer = String.fromEnvironment(
|
static const _configuredOidcIssuer = String.fromEnvironment(
|
||||||
'AIOA_OIDC_ISSUER',
|
'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
|
static String get apiBaseUrl => _configuredApiBaseUrl.isNotEmpty
|
||||||
? _configuredApiBaseUrl
|
? _configuredApiBaseUrl
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class DynamicFormCard extends StatelessWidget {
|
|||||||
onChanged: (value) => onChanged(control.field, value),
|
onChanged: (value) => onChanged(control.field, value),
|
||||||
),
|
),
|
||||||
FormControlType.dateTime => _DateTimeControl(
|
FormControlType.dateTime => _DateTimeControl(
|
||||||
|
key: ValueKey(control.field),
|
||||||
label: label,
|
label: label,
|
||||||
value: state.values[control.field] as String?,
|
value: state.values[control.field] as String?,
|
||||||
errorText: error,
|
errorText: error,
|
||||||
@@ -109,6 +110,7 @@ class DynamicFormCard extends StatelessWidget {
|
|||||||
|
|
||||||
class _DateTimeControl extends StatelessWidget {
|
class _DateTimeControl extends StatelessWidget {
|
||||||
const _DateTimeControl({
|
const _DateTimeControl({
|
||||||
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.errorText,
|
required this.errorText,
|
||||||
|
|||||||
@@ -246,6 +246,11 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.1"
|
version: "12.0.1"
|
||||||
|
flutter_driver:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -336,6 +341,11 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
fuchsia_remote_debug_protocol:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -384,6 +394,11 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
integration_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -600,6 +615,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.2"
|
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:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -773,6 +796,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
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:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -869,6 +900,14 @@ packages:
|
|||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
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:
|
webkit_inspection_protocol:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ dependencies:
|
|||||||
shared_preferences: ^2.5.3
|
shared_preferences: ^2.5.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
integration_test:
|
||||||
|
sdk: flutter
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,5 @@
|
|||||||
`verify-local-auth.sh` 使用 Keycloak Realm 中明确标记为仅限本地开发的账号,注册脚本设备并验证健康检查、Token 获取、设备会话和 `/api/v1/me`。不得把该脚本及其测试凭据用于共享或生产环境。
|
`verify-local-auth.sh` 使用 Keycloak Realm 中明确标记为仅限本地开发的账号,注册脚本设备并验证健康检查、Token 获取、设备会话和 `/api/v1/me`。不得把该脚本及其测试凭据用于共享或生产环境。
|
||||||
|
|
||||||
`verify-all.sh` 执行后端测试、AI 服务测试、Flutter 格式/分析/测试、OpenAPI YAML 和 Git 空白检查。设置 `AIOA_FULL_BUILD=1` 后还会构建 Android Debug APK,并在 macOS 上构建 iOS Simulator App。
|
`verify-all.sh` 执行后端测试、AI 服务测试、Flutter 格式/分析/测试、OpenAPI YAML 和 Git 空白检查。设置 `AIOA_FULL_BUILD=1` 后还会构建 Android Debug APK,并在 macOS 上构建 iOS Simulator App。
|
||||||
|
|
||||||
|
`test-ios-mvp.sh` 在已启动的 iOS Simulator 上依次执行员工、部门主管和 OA 管理员真实前端业务场景。测试连接本地 Keycloak、后端、PostgreSQL 和 Flowable,并自动构造互相隔离的业务数据。
|
||||||
|
|||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
keycloak_url="${KEYCLOAK_URL:-http://localhost:8081}"
|
||||||
|
backend_url="${BACKEND_URL:-http://localhost:8080}"
|
||||||
|
device_id="${IOS_DEVICE_ID:-$(xcrun simctl list devices booted -j | jq -r '.devices[][] | select(.state == "Booted") | .udid' | head -1)}"
|
||||||
|
|
||||||
|
if [[ -z "${device_id}" ]]; then
|
||||||
|
echo 'No booted iOS simulator. Boot one or set IOS_DEVICE_ID.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl --fail --silent "${backend_url}/actuator/health" >/dev/null
|
||||||
|
|
||||||
|
token() {
|
||||||
|
curl --fail --silent --show-error \
|
||||||
|
-X POST "${keycloak_url}/realms/aioa/protocol/openid-connect/token" \
|
||||||
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode client_id=aioa-mobile \
|
||||||
|
--data-urlencode grant_type=password \
|
||||||
|
--data-urlencode "username=$1" \
|
||||||
|
--data-urlencode "password=$2" | jq -r .access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
run_role() {
|
||||||
|
local role="$1"
|
||||||
|
local access_token="$2"
|
||||||
|
local employee_access_token="$3"
|
||||||
|
echo "Running iOS MVP scenario: ${role}"
|
||||||
|
(
|
||||||
|
cd "${root_dir}/mobile"
|
||||||
|
flutter test integration_test/mvp_business_test.dart \
|
||||||
|
-d "${device_id}" \
|
||||||
|
--dart-define="AIOA_E2E_ACCESS_TOKEN=${access_token}" \
|
||||||
|
--dart-define="AIOA_E2E_EMPLOYEE_TOKEN=${employee_access_token}" \
|
||||||
|
--dart-define="AIOA_E2E_ROLE=${role}" \
|
||||||
|
--dart-define="AIOA_API_BASE_URL=${backend_url}/api/v1"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
employee_token="$(token employee 'Employee123!')"
|
||||||
|
run_role employee "${employee_token}" "${employee_token}"
|
||||||
|
|
||||||
|
employee_token="$(token employee 'Employee123!')"
|
||||||
|
manager_token="$(token manager 'Manager123!')"
|
||||||
|
run_role manager "${manager_token}" "${employee_token}"
|
||||||
|
|
||||||
|
admin_token="$(token admin 'Admin123!')"
|
||||||
|
run_role admin "${admin_token}" "${employee_token}"
|
||||||
|
|
||||||
|
echo 'All iOS MVP frontend business scenarios passed.'
|
||||||
Reference in New Issue
Block a user