feat: 对齐常驻商城与七鱼客服交互

- 同步商城 H5 登录态与原生 token
- 完善商城/原生 tab 切换、动画及返回交互
- 修复七鱼客服键盘避让和状态栏恢复
- 调整开屏跳过按钮与登录入口交互
This commit is contained in:
jingyun committed 2026-09-07 15:47:01 +08:00
1 parent b4343ea528
commit 3b089e2205
37 files changed
+2970 -375

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import 'package:dsbridge_flutter/dsbridge_flutter.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/presentation/js_api.dart';
void main() {
test('close invokes the host callback and returns success', () {
var closeCalls = 0;
final handler = _FakeCompletionHandler();
final api = JsApi(onClose: () => closeCalls++);
api.close(null, handler);
expect(closeCalls, 1);
expect(handler.result, <String, dynamic>{'code': 0, 'message': 'success'});
});
test('coerces JSON mall state payloads', () {
expect(
coerceMallStateMap(
'{"ready":true,"activeTab":"market","canGoBack":false}',
),
<String, dynamic>{
'ready': true,
'activeTab': 'market',
'canGoBack': false,
},
);
});
}
class _FakeCompletionHandler implements CompletionHandler {
dynamic result;
@override
void complete([dynamic retValue]) => result = retValue;
@override
void setProgressData(dynamic value) {}
}
@@ -0,0 +1,54 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/application/mall_bridge_state.dart';
void main() {
test('updates state and forwards tab interactions before ready', () async {
final bridge = MallBridgeController.instance;
bridge.detach();
addTearDown(bridge.detach);
MallBridgeTab? activated;
bool? reselected;
var backCalls = 0;
bridge.attach(
activateTab: (tab, reselect) async {
activated = tab;
reselected = reselect;
},
goBack: () async => backCalls++,
);
await bridge.activate(MallBridgeTab.orders);
expect(activated, MallBridgeTab.orders);
expect(bridge.state.value.activeTab, MallBridgeTab.orders);
bridge.update(const {
'ready': true,
'activeTab': 'orders',
'canGoBack': true,
});
await bridge.activate(MallBridgeTab.orders, reselect: true);
await bridge.goBack();
expect(bridge.state.value.activeTab, MallBridgeTab.orders);
expect(bridge.state.value.canGoBack, isTrue);
expect(activated, MallBridgeTab.orders);
expect(reselected, isTrue);
expect(backCalls, 1);
});
test('accepts numeric and string booleans from H5', () {
final bridge = MallBridgeController.instance;
bridge.detach();
addTearDown(bridge.detach);
bridge.update(const {
'ready': 1,
'activeTab': 'market',
'canGoBack': 'true',
});
expect(bridge.state.value.ready, isTrue);
expect(bridge.state.value.activeTab, MallBridgeTab.market);
expect(bridge.state.value.canGoBack, isTrue);
});
}
@@ -0,0 +1,62 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/application/mall_web_host_controller.dart';
void main() {
final host = MallWebHostController.instance;
setUp(() async => host.clear());
tearDown(() async => host.clear());
test('opens and dismisses the resident full-screen mall host', () {
final uri = Uri.parse('https://mall.example.com/?isNight=0');
host.open(uri);
expect(host.state.value.url, uri.toString());
expect(host.state.value.visible, isTrue);
host.dismiss();
expect(host.state.value.url, uri.toString());
expect(host.state.value.visible, isFalse);
});
test('cache identity ignores attribution but includes theme', () {
const first =
'https://mall.example.com/path?isNight=0&click_id=a&tracking_id=t1';
const second =
'https://mall.example.com/path?isNight=0&click_id=b&tracking_id=t2';
const dark = 'https://mall.example.com/path?isNight=1&click_id=b';
expect(
MallWebHostController.cacheKeyFor(first),
MallWebHostController.cacheKeyFor(second),
);
expect(
MallWebHostController.cacheKeyFor(first),
isNot(MallWebHostController.cacheKeyFor(dark)),
);
});
test('defers a changed preload while the mall is visible', () {
final active = Uri.parse('https://mall.example.com/?isNight=0');
final refreshed = Uri.parse('https://mall.example.com/?isNight=1');
host.open(active);
host.preload(refreshed);
expect(host.state.value.url, active.toString());
expect(host.state.value.visible, isTrue);
host.dismiss();
expect(host.state.value.url, refreshed.toString());
expect(host.state.value.visible, isFalse);
});
test('themed URL replaces a stale isNight value', () {
final uri = MallWebHostController.uriForTheme(
Uri.parse('https://mall.example.com/?from=jinzhi&isNight=1'),
isDark: false,
);
expect(uri.queryParameters['from'], 'jinzhi');
expect(uri.queryParameters['isNight'], '0');
});
}
+62
View File
@@ -0,0 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/presentation/mall_web_host.dart';
void main() {
test('loading only covers the WebView before its first page', () {
expect(
shouldShowMallLoading(
hasError: false,
hasDisplayedPage: false,
progress: 0,
),
isTrue,
);
expect(
shouldShowMallLoading(
hasError: false,
hasDisplayedPage: true,
progress: 0,
),
isFalse,
);
expect(
shouldShowMallLoading(
hasError: true,
hasDisplayedPage: false,
progress: 0,
),
isFalse,
);
});
test('only Android Qiyu pages reserve keyboard space for the WebView', () {
expect(
qiyuWebViewBottomInset(
isShowingQiyuPage: true,
platform: TargetPlatform.android,
keyboardInset: 336,
isWeb: false,
),
336,
);
expect(
qiyuWebViewBottomInset(
isShowingQiyuPage: false,
platform: TargetPlatform.android,
keyboardInset: 336,
isWeb: false,
),
0,
);
expect(
qiyuWebViewBottomInset(
isShowingQiyuPage: true,
platform: TargetPlatform.iOS,
keyboardInset: 336,
isWeb: false,
),
0,
);
});
}
@@ -0,0 +1,25 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/application/web_back_navigation.dart';
void main() {
test('uses browser history only when it exists', () async {
var wentBack = false;
expect(
await tryGoBackInWebHistory(
canGoBack: () async => false,
goBack: () async => wentBack = true,
),
isFalse,
);
expect(wentBack, isFalse);
});
test('recognizes DSBridge JavaScript boolean results', () async {
expect(
await tryGoBackUsingVisibleWebControl(
runJavaScriptReturningResult: (_) async => 'true',
),
isTrue,
);
});
}
@@ -0,0 +1,32 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jinzhi_flutter_app/features/web/application/web_navigation_policy.dart';
void main() {
test('external handoff matches the resident mall policy', () {
expect(
shouldOpenWebUrlExternally('https://example.com/download?out=1'),
isTrue,
);
expect(shouldOpenWebUrlExternally('https://example.com/order/1'), isFalse);
expect(shouldOpenWebUrlExternally('tel:4000450268'), isTrue);
});
test('telephone handoff removes H5 parameters', () {
expect(
sanitizeSystemCommunicationUri(Uri.parse('tel:4000450268?out=1')),
Uri.parse('tel:4000450268'),
);
expect(
sanitizeSystemCommunicationUri(
Uri.parse('tel:4000450268&campaign=mall&out=1'),
),
Uri.parse('tel:4000450268'),
);
});
test('trusts only exact Qiyu domains and subdomains', () {
expect(isTrustedQiyuUrl('https://qiyukf.com/client'), isTrue);
expect(isTrustedQiyuUrl('https://service.qiyukf.cn/client'), isTrue);
expect(isTrustedQiyuUrl('https://qiyukf.com.evil.example/client'), isFalse);
});
}