app scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:29:55 +08:00
commit 681688dfae
301 changed files with 18414 additions and 0 deletions
@@ -0,0 +1,102 @@
// core_router 的高价值断言:
// 1. 未选店时**绝不能**放行到业务页——那边 currentStoreIdProvider 会直接 throw。
// 2. 会话未就绪时停在启动页,不能提前放行。
// 3. H5 路由里只有 target,永远不出现裸 URL(安全)。
import 'package:core_auth/core_auth.dart';
import 'package:core_router/core_router.dart';
import 'package:flutter_test/flutter_test.dart';
const UserContext _user = UserContext(
userId: 'U1',
employeeId: 'E1',
phone: '13800000000',
roleCode: 'CLERK',
channel: 'APP',
);
const StoreContext _store = StoreContext(
storeId: 7,
storeCode: 'S007',
storeName: '测试门店',
orgId: 1,
);
void main() {
group('resolveRedirect', () {
test('会话未就绪时停在启动页,不提前放行到业务页', () {
expect(resolveRedirect(null, '/purchase/orders', Uri.parse('/purchase/orders')), '/splash');
expect(resolveRedirect(null, '/splash', Uri.parse('/splash')), isNull);
expect(resolveRedirect(const SessionLoading(), '/home', Uri.parse('/home')), '/splash');
});
test('未登录访问业务页 → 登录页并带上回跳目标', () {
final String? to = resolveRedirect(
const SessionUnauthenticated(),
'/purchase/orders',
Uri.parse('/purchase/orders?id=9'),
);
expect(to, startsWith('/login?from='));
expect(Uri.decodeComponent(Uri.parse(to!).queryParameters['from']!), '/purchase/orders?id=9');
});
test('未登录停在登录页时不再跳转(否则死循环)', () {
expect(
resolveRedirect(const SessionUnauthenticated(), '/login', Uri.parse('/login')),
isNull,
);
});
test('已登录但未选店时,任何业务页都被挡回选店页', () {
const AppSession awaiting = SessionAwaitingStore(user: _user, candidates: <StoreContext>[]);
// 这条是核心:放行过去 currentStoreIdProvider 会 throw11)。
expect(resolveRedirect(awaiting, '/home', Uri.parse('/home')), '/store-picker');
expect(resolveRedirect(awaiting, '/store-picker', Uri.parse('/store-picker')), isNull);
});
group('已登录且已选店', () {
const AppSession active = SessionActive(user: _user, store: _store);
test('停在启动页/选店页时进工作台', () {
expect(resolveRedirect(active, '/splash', Uri.parse('/splash')), '/home');
expect(resolveRedirect(active, '/store-picker', Uri.parse('/store-picker')), '/home');
});
test('登录页带 from 时回跳原目标,没有 from 时进工作台', () {
expect(
resolveRedirect(
active,
'/login',
Uri.parse('/login?from=${Uri.encodeComponent('/purchase/orders?id=9')}'),
),
'/purchase/orders?id=9',
);
expect(resolveRedirect(active, '/login', Uri.parse('/login')), '/home');
});
test('业务页放行', () {
expect(resolveRedirect(active, '/purchase/orders', Uri.parse('/purchase/orders')), isNull);
});
});
});
group('菜单与 H5 路由', () {
test('未知菜单 code 返回 null,由调用方隐藏并上报', () {
expect(resolveMenuRoute('PURCHASE_ORDER'), '/purchase/orders');
expect(resolveMenuRoute('SOMETHING_NEW_FROM_BACKEND'), isNull);
});
test('H5 路由只带 target,不出现裸 URL', () {
final String route = AppRoutes.webviewFor('QUOTE_ORDER', title: '报价开单');
expect(route, startsWith('/webview?target=QUOTE_ORDER'));
expect(route, isNot(contains('http')));
expect(Uri.parse(route).queryParameters['title'], '报价开单');
});
test('菜单表里的 H5 项也是 target 形态', () {
for (final String path in menuRouteMap.values) {
expect(path, isNot(contains('http')), reason: '路由里塞裸 URL 是明确的安全洞');
}
});
});
}