feat: 对齐常驻商城与七鱼客服交互
- 同步商城 H5 登录态与原生 token - 完善商城/原生 tab 切换、动画及返回交互 - 修复七鱼客服键盘避让和状态栏恢复 - 调整开屏跳过按钮与登录入口交互
This commit is contained in:
1 parent
b4343ea528
commit
3b089e2205
37 files changed
+2970
-375
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/ads/data/ads_api_client.dart';
|
||||
|
||||
void main() {
|
||||
test('protobuf strings are decoded as UTF-8', () {
|
||||
final value = utf8.encode('商城');
|
||||
final message = ProtoMessage(<int>[10, value.length, ...value]);
|
||||
|
||||
expect(message.string(1), '商城');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||
|
||||
void main() {
|
||||
test('preserves Jinzhi attribution while syncing H5 login', () {
|
||||
final arguments = buildEnterWebMallBridgeArguments(
|
||||
currentUri: Uri.parse(
|
||||
'https://mall.example.com/?from=jinzhi&source_app=jinzhi'
|
||||
'&source_entry=mall_tab&placement=mall_tab&hook_type=mall_tab'
|
||||
'&tracking_id=tracking&click_id=click-1&ts=123&isNight=0',
|
||||
),
|
||||
logged: true,
|
||||
isDark: true,
|
||||
authToken: 'mall-token',
|
||||
scene: 'web_mall',
|
||||
sourceApp: 'jinzhi',
|
||||
attribution: const MallClickAttribution(
|
||||
adId: 'latest-ad',
|
||||
trackingId: 'latest-tracking',
|
||||
clickId: 'latest-click',
|
||||
),
|
||||
);
|
||||
|
||||
expect(arguments['from'], 'jinzhi');
|
||||
expect(arguments['source_app'], 'jinzhi');
|
||||
expect(arguments['ad_id'], 'latest-ad');
|
||||
expect(arguments['tracking_id'], 'latest-tracking');
|
||||
expect(arguments['click_id'], 'latest-click');
|
||||
expect(arguments['isNight'], '1');
|
||||
expect(arguments['auth_token'], 'mall-token');
|
||||
expect(arguments['authToken'], 'mall-token');
|
||||
expect(arguments['scene'], 'web_mall');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||
|
||||
void main() {
|
||||
test('builds the resident mall URL with Jinzhi attribution and theme', () {
|
||||
const config = MallTabConfig(
|
||||
enabled: true,
|
||||
url: 'https://mall.example.com/home?campaign=gold',
|
||||
adId: 'ad-1',
|
||||
trackingId: 'tracking-1',
|
||||
);
|
||||
|
||||
final uri = config.attributedUri(clickId: 'click-1', isDark: true)!;
|
||||
|
||||
expect(uri.queryParameters['campaign'], 'gold');
|
||||
expect(uri.queryParameters['source_app'], 'jinzhi');
|
||||
expect(uri.queryParameters['source_entry'], 'mall_tab');
|
||||
expect(uri.queryParameters['ad_id'], 'ad-1');
|
||||
expect(uri.queryParameters['tracking_id'], 'tracking-1');
|
||||
expect(uri.queryParameters['click_id'], 'click-1');
|
||||
expect(uri.queryParameters['isNight'], '1');
|
||||
});
|
||||
|
||||
test('retains server open-mode metadata', () {
|
||||
const resident = MallTabConfig(
|
||||
enabled: true,
|
||||
url: 'https://mall.example.com',
|
||||
);
|
||||
const nativeApp = MallTabConfig(
|
||||
enabled: true,
|
||||
url: 'https://mall.example.com',
|
||||
openMode: 'native_app',
|
||||
);
|
||||
const external = MallTabConfig(
|
||||
enabled: true,
|
||||
url: 'https://mall.example.com',
|
||||
openMode: 'external',
|
||||
);
|
||||
|
||||
expect(resident.opensInApp, isTrue);
|
||||
expect(nativeApp.opensInApp, isFalse);
|
||||
expect(external.opensInApp, isFalse);
|
||||
});
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user