Merge branch 'main' of https://gitea.23544.com/wangyang/web_shell_flutter
This commit is contained in:
commit
40d1b179d0
|
|
@ -3,5 +3,6 @@
|
||||||
"preferredOrientations": [
|
"preferredOrientations": [
|
||||||
"portraitUp",
|
"portraitUp",
|
||||||
"portraitDown"
|
"portraitDown"
|
||||||
]
|
],
|
||||||
|
"upgradeConfigUrl": "https://umsapi.23544.com/api/biz/version/get-latest-config?appId=787154824761413&environment=1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ application_id: "com.yuanxuan.aixue"
|
||||||
app_key: "aixue_prod"
|
app_key: "aixue_prod"
|
||||||
default_url: "http://xszy.lzzneng.com/login.html"
|
default_url: "http://xszy.lzzneng.com/login.html"
|
||||||
bootstrap_config_url: ""
|
bootstrap_config_url: ""
|
||||||
upgrade_config_url: ""
|
upgrade_config_url: "https://umsapi.23544.com/api/biz/version/get-latest-config?appId=787154824761413&environment=1"
|
||||||
preferred_orientations:
|
preferred_orientations:
|
||||||
- "portraitUp"
|
- "portraitUp"
|
||||||
- "portraitDown"
|
- "portraitDown"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||||
|
import 'package:yx_app_upgrade_flutter/app_upgrade_plugin.dart';
|
||||||
import 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart';
|
import 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart';
|
||||||
|
|
||||||
export 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart'
|
export 'package:yx_app_upgrade_flutter/yx_app_upgrade_flutter.dart'
|
||||||
|
|
@ -54,6 +55,7 @@ part 'src/ui/unsupported_platform_page.dart';
|
||||||
|
|
||||||
// ── 全局环境 ──
|
// ── 全局环境 ──
|
||||||
late ShellEnvironment _env;
|
late ShellEnvironment _env;
|
||||||
|
final ValueNotifier<int> _runtimeConfigVersion = ValueNotifier<int>(0);
|
||||||
|
|
||||||
Color get _shellAccentColor => _env.accentColor;
|
Color get _shellAccentColor => _env.accentColor;
|
||||||
Color get _shellBackgroundColor => _env.backgroundColor;
|
Color get _shellBackgroundColor => _env.backgroundColor;
|
||||||
|
|
@ -83,6 +85,41 @@ Future<void> runShellApp(ShellEnvironment environment) async {
|
||||||
runApp(const ShellApp());
|
runApp(const ShellApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重新请求升级配置,并将其中的运行时启动配置应用到当前壳环境。
|
||||||
|
Future<ShellUpgradeConfig?> reloadUpgradeRuntimeConfig() async {
|
||||||
|
final configUrl = ShellUpgradeService.instance._configUrl?.trim();
|
||||||
|
if (configUrl == null || configUrl.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteConfig = await ShellUpgradeService.instance._fetchConfig(
|
||||||
|
configUrl,
|
||||||
|
);
|
||||||
|
if (remoteConfig == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _applyUpgradeRuntimeConfig(remoteConfig, source: '升级配置');
|
||||||
|
return remoteConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 先重新拉取升级配置中的运行时启动配置,再执行升级检查。
|
||||||
|
Future<ShellUpgradeConfig?> reloadUpgradeRuntimeConfigAndCheckVersion(
|
||||||
|
BuildContext context, {
|
||||||
|
bool showNoUpdateToast = false,
|
||||||
|
}) async {
|
||||||
|
final remoteConfig = await reloadUpgradeRuntimeConfig();
|
||||||
|
if (remoteConfig == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShellUpgradeService.instance.checkVersion(
|
||||||
|
context,
|
||||||
|
showNoUpdateToast: showNoUpdateToast,
|
||||||
|
);
|
||||||
|
return remoteConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/// 返回最终确定的 upgradeConfigUrl。
|
/// 返回最终确定的 upgradeConfigUrl。
|
||||||
Future<String?> _applyBootstrapConfig() async {
|
Future<String?> _applyBootstrapConfig() async {
|
||||||
String? bootstrapConfigUrl;
|
String? bootstrapConfigUrl;
|
||||||
|
|
@ -145,6 +182,38 @@ void _mergeBootstrapConfig(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _applyUpgradeRuntimeConfig(
|
||||||
|
ShellUpgradeConfig remoteConfig, {
|
||||||
|
required String source,
|
||||||
|
}) async {
|
||||||
|
final runtimeConfig = remoteConfig.config;
|
||||||
|
if (runtimeConfig == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final previousInitialUrl = _env.initialUrl?.trim();
|
||||||
|
final previousPreferredOrientations = _env.preferredOrientations;
|
||||||
|
|
||||||
|
_mergeBootstrapConfig(runtimeConfig, source: source);
|
||||||
|
|
||||||
|
final nextUpgradeConfigUrl = runtimeConfig.upgradeConfigUrl?.trim();
|
||||||
|
if (nextUpgradeConfigUrl != null && nextUpgradeConfigUrl.isNotEmpty) {
|
||||||
|
ShellUpgradeService.instance.setupConfigUrl(nextUpgradeConfigUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initializeUrls();
|
||||||
|
await SystemChrome.setPreferredOrientations(_shellPreferredOrientations);
|
||||||
|
|
||||||
|
final hasInitialUrlChanged = previousInitialUrl != _env.initialUrl?.trim();
|
||||||
|
final hasOrientationsChanged = !listEquals(
|
||||||
|
previousPreferredOrientations,
|
||||||
|
_env.preferredOrientations,
|
||||||
|
);
|
||||||
|
if (hasInitialUrlChanged || hasOrientationsChanged) {
|
||||||
|
_runtimeConfigVersion.value += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _enterImmersiveMode() async {
|
Future<void> _enterImmersiveMode() async {
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,21 @@ void _setStatusBarFromBridge(Map<String, dynamic> payload) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _runtimeConfigSnapshotFromBridge(
|
||||||
|
ShellUpgradeConfig? remoteConfig,
|
||||||
|
) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'initialUrl': _initialUrl,
|
||||||
|
'preferredOrientations': _shellPreferredOrientations
|
||||||
|
.map((item) => item.name)
|
||||||
|
.toList(),
|
||||||
|
'upgradeConfigUrl': ShellUpgradeService.instance._configUrl,
|
||||||
|
'versionName': remoteConfig?.versionName,
|
||||||
|
'version': remoteConfig?.version,
|
||||||
|
'describe': remoteConfig?.describe,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Toast 动效组件 ──
|
// ── Toast 动效组件 ──
|
||||||
|
|
||||||
class _ToastWidget extends StatefulWidget {
|
class _ToastWidget extends StatefulWidget {
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,11 @@ String _buildAppShellBridgeScript() {
|
||||||
openExternal: (url) => send('openExternal', { url }),
|
openExternal: (url) => send('openExternal', { url }),
|
||||||
requestPermissions: (types = []) => send('requestPermissions', { types }),
|
requestPermissions: (types = []) => send('requestPermissions', { types }),
|
||||||
reloadPage: () => send('reloadPage'),
|
reloadPage: () => send('reloadPage'),
|
||||||
|
reloadUpgradeRuntimeConfig: () => send('reloadUpgradeRuntimeConfig'),
|
||||||
|
reloadUpgradeRuntimeConfigAndCheckVersion: (options = {}) => send(
|
||||||
|
'reloadUpgradeRuntimeConfigAndCheckVersion',
|
||||||
|
options,
|
||||||
|
),
|
||||||
goBack: () => send('goBack'),
|
goBack: () => send('goBack'),
|
||||||
closeApp: () => send('closeApp'),
|
closeApp: () => send('closeApp'),
|
||||||
getDeviceInfo: () => send('getDeviceInfo'),
|
getDeviceInfo: () => send('getDeviceInfo'),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
part of '../../core_app.dart';
|
part of '../../core_app.dart';
|
||||||
|
|
||||||
/// 远程升级配置模型。
|
/// 升级明细配置,对应升级接口中的 `upgrade` 字段。
|
||||||
class ShellUpgradeConfig {
|
class ShellUpgradeReleaseConfig {
|
||||||
final String? versionName;
|
final String? versionName;
|
||||||
final int? version;
|
final int? version;
|
||||||
final int? isForce;
|
final int? isForce;
|
||||||
|
|
@ -9,7 +9,7 @@ class ShellUpgradeConfig {
|
||||||
final String? filePath;
|
final String? filePath;
|
||||||
final int? fileSize;
|
final int? fileSize;
|
||||||
|
|
||||||
ShellUpgradeConfig({
|
const ShellUpgradeReleaseConfig({
|
||||||
this.versionName,
|
this.versionName,
|
||||||
this.version,
|
this.version,
|
||||||
this.isForce,
|
this.isForce,
|
||||||
|
|
@ -18,31 +18,268 @@ class ShellUpgradeConfig {
|
||||||
this.fileSize,
|
this.fileSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory ShellUpgradeReleaseConfig.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ShellUpgradeReleaseConfig(
|
||||||
|
versionName: _readShellUpgradeString(json['versionName']),
|
||||||
|
version: _readShellUpgradeInt(json['version']),
|
||||||
|
isForce: _readShellUpgradeInt(json['isForce']),
|
||||||
|
remark: _readShellUpgradeString(json['remark']),
|
||||||
|
filePath: _readShellUpgradeString(json['filePath']),
|
||||||
|
fileSize: _readShellUpgradeInt(json['fileSize']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 兼容已经解码好的 Map,或 `data.config.upgrade` 这种 JSON 字符串。
|
||||||
|
static ShellUpgradeReleaseConfig? fromDynamic(dynamic value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value is Map<String, dynamic>) {
|
||||||
|
return ShellUpgradeReleaseConfig.fromJson(value);
|
||||||
|
}
|
||||||
|
if (value is Map) {
|
||||||
|
return ShellUpgradeReleaseConfig.fromJson(
|
||||||
|
Map<String, dynamic>.from(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value is! String) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final trimmed = value.trim();
|
||||||
|
if (trimmed.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(trimmed);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
return ShellUpgradeReleaseConfig.fromJson(decoded);
|
||||||
|
}
|
||||||
|
if (decoded is Map) {
|
||||||
|
return ShellUpgradeReleaseConfig.fromJson(
|
||||||
|
Map<String, dynamic>.from(decoded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('解析 WebShell 升级配置 upgrade 字段异常: $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 远程升级配置模型,对应升级接口的顶层 JSON。
|
||||||
|
///
|
||||||
|
/// 当前仅解析新接口结构:
|
||||||
|
/// 1. 顶层包含 `success/code/msg/data`;
|
||||||
|
/// 2. 升级与启动配置都放在 `data.config` 这个 JSON 字符串中;
|
||||||
|
/// 3. `data.config` 解开后,内部再拆为 `upgrade` 和 `config` 两部分。
|
||||||
|
class ShellUpgradeConfig {
|
||||||
|
final bool? success;
|
||||||
|
final String? responseCode;
|
||||||
|
final String? msg;
|
||||||
|
final int? id;
|
||||||
|
final int? upgradeFileId;
|
||||||
|
final int? installFileId;
|
||||||
|
final bool? isLatest;
|
||||||
|
final int? appId;
|
||||||
|
final String? releaseCode;
|
||||||
|
final String? releaseTime;
|
||||||
|
final bool? releaseStatus;
|
||||||
|
final String? upgradeOssUrl;
|
||||||
|
final bool? isAllowRollback;
|
||||||
|
final String? rawConfig;
|
||||||
|
final String? configDescribe;
|
||||||
|
final int? describeType;
|
||||||
|
final int? environment;
|
||||||
|
final String? createdTime;
|
||||||
|
final ShellUpgradeReleaseConfig? upgrade;
|
||||||
|
final ShellBootstrapConfig? config;
|
||||||
|
final String? installOssUrl;
|
||||||
|
final String? describe;
|
||||||
|
|
||||||
|
const ShellUpgradeConfig({
|
||||||
|
this.success,
|
||||||
|
this.responseCode,
|
||||||
|
this.msg,
|
||||||
|
this.id,
|
||||||
|
this.upgradeFileId,
|
||||||
|
this.installFileId,
|
||||||
|
this.isLatest,
|
||||||
|
this.appId,
|
||||||
|
this.releaseCode,
|
||||||
|
this.releaseTime,
|
||||||
|
this.releaseStatus,
|
||||||
|
this.upgradeOssUrl,
|
||||||
|
this.isAllowRollback,
|
||||||
|
this.rawConfig,
|
||||||
|
this.configDescribe,
|
||||||
|
this.describe,
|
||||||
|
this.describeType,
|
||||||
|
this.environment,
|
||||||
|
this.createdTime,
|
||||||
|
this.installOssUrl,
|
||||||
|
this.upgrade,
|
||||||
|
this.config,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? get versionName => upgrade?.versionName;
|
||||||
|
|
||||||
|
int? get version => upgrade?.version;
|
||||||
|
|
||||||
|
int? get isForce => upgrade?.isForce;
|
||||||
|
|
||||||
|
String? get remark => describe ?? upgrade?.remark;
|
||||||
|
|
||||||
|
String? get filePath =>
|
||||||
|
_readShellUpgradeString(installOssUrl ?? upgrade?.filePath);
|
||||||
|
|
||||||
|
int? get fileSize => upgrade?.fileSize;
|
||||||
|
|
||||||
|
bool get hasStructuredPayload => upgrade != null || config != null;
|
||||||
|
|
||||||
|
/// 仅使用远端 `upgrade.version` 与本地 buildNumber 做整数比较。
|
||||||
|
///
|
||||||
|
/// 返回 `true` 表示远端版本更高,需要触发升级提示;
|
||||||
|
/// 如果远端未下发版本号,则直接视为无需升级。
|
||||||
|
bool shouldOfferUpgrade({required int localVersion}) {
|
||||||
|
final remoteVersion = upgrade?.version;
|
||||||
|
if (remoteVersion == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return remoteVersion > localVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析接口响应,并从 `data.config` 中拆出:
|
||||||
|
/// - `upgrade`:升级弹窗所需信息
|
||||||
|
/// - `config`:启动期运行时配置,如 initialUrl / preferredOrientations
|
||||||
factory ShellUpgradeConfig.fromJson(Map<String, dynamic> json) {
|
factory ShellUpgradeConfig.fromJson(Map<String, dynamic> json) {
|
||||||
|
final data = json['data'];
|
||||||
|
final dataMap = data is Map<String, dynamic>
|
||||||
|
? data
|
||||||
|
: data is Map
|
||||||
|
? Map<String, dynamic>.from(data)
|
||||||
|
: const <String, dynamic>{};
|
||||||
|
final rawConfig = _readShellUpgradeString(dataMap['config']);
|
||||||
|
final parsedConfig = _readShellUpgradeEmbeddedConfig(rawConfig);
|
||||||
|
final rawBootstrapConfig = parsedConfig?['config'];
|
||||||
|
final bootstrapConfig = rawBootstrapConfig is Map<String, dynamic>
|
||||||
|
? ShellBootstrapConfig.fromJson(rawBootstrapConfig)
|
||||||
|
: rawBootstrapConfig is Map
|
||||||
|
? ShellBootstrapConfig.fromJson(
|
||||||
|
Map<String, dynamic>.from(rawBootstrapConfig),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return ShellUpgradeConfig(
|
return ShellUpgradeConfig(
|
||||||
versionName: json['versionName']?.toString(),
|
success: _readShellUpgradeBool(json['success']),
|
||||||
version: json['version'] as int?,
|
responseCode: _readShellUpgradeString(json['code']),
|
||||||
isForce: (json['isForce'] ?? json['isforce']) as int?,
|
msg: _readShellUpgradeString(json['msg']),
|
||||||
remark: json['remark']?.toString(),
|
id: _readShellUpgradeInt(dataMap['id']),
|
||||||
filePath: json['filePath']?.toString(),
|
upgradeFileId: _readShellUpgradeInt(dataMap['upgradeFileId']),
|
||||||
fileSize: json['fileSize'] as int?,
|
installFileId: _readShellUpgradeInt(dataMap['installFileId']),
|
||||||
|
isLatest: _readShellUpgradeBool(dataMap['isLatest']),
|
||||||
|
appId: _readShellUpgradeInt(dataMap['appId']),
|
||||||
|
releaseCode: _readShellUpgradeString(dataMap['code']),
|
||||||
|
releaseTime: _readShellUpgradeString(dataMap['releaseTime']),
|
||||||
|
releaseStatus: _readShellUpgradeBool(dataMap['releaseStatus']),
|
||||||
|
upgradeOssUrl: _readShellUpgradeString(dataMap['upgradeOssUrl']),
|
||||||
|
installOssUrl: _readShellUpgradeString(dataMap['installOssUrl']),
|
||||||
|
isAllowRollback: _readShellUpgradeBool(dataMap['isAllowRollback']),
|
||||||
|
rawConfig: rawConfig,
|
||||||
|
upgrade: ShellUpgradeReleaseConfig.fromDynamic(parsedConfig?['upgrade']),
|
||||||
|
config: bootstrapConfig,
|
||||||
|
configDescribe: _readShellUpgradeString(dataMap['configDescribe']),
|
||||||
|
describe: _readShellUpgradeString(dataMap['describe']),
|
||||||
|
describeType: _readShellUpgradeInt(dataMap['describeType']),
|
||||||
|
environment: _readShellUpgradeInt(dataMap['environment']),
|
||||||
|
createdTime: _readShellUpgradeString(dataMap['createdTime']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 管理壳应用的升级逻辑,依赖 `yx_app_upgrade_flutter`。
|
String? _readShellUpgradeString(dynamic value) {
|
||||||
|
final normalized = value?.toString().trim();
|
||||||
|
if (normalized == null || normalized.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _readShellUpgradeInt(dynamic value) {
|
||||||
|
if (value is int) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value.toInt();
|
||||||
|
}
|
||||||
|
final normalized = value?.toString().trim();
|
||||||
|
if (normalized == null || normalized.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return int.tryParse(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool? _readShellUpgradeBool(dynamic value) {
|
||||||
|
if (value is bool) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value != 0;
|
||||||
|
}
|
||||||
|
final normalized = value?.toString().trim().toLowerCase();
|
||||||
|
if (normalized == null || normalized.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (normalized == 'true' || normalized == '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (normalized == 'false' || normalized == '0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 `data.config` 这个嵌套 JSON 字符串。
|
||||||
|
Map<String, dynamic>? _readShellUpgradeEmbeddedConfig(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(value);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
if (decoded is Map) {
|
||||||
|
return Map<String, dynamic>.from(decoded);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('解析 WebShell 升级配置 data.config 异常: $e');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 管理壳应用的升级逻辑,依赖 yx_app_upgrade_flutter。
|
||||||
class ShellUpgradeService {
|
class ShellUpgradeService {
|
||||||
static final ShellUpgradeService instance = ShellUpgradeService._();
|
static final ShellUpgradeService instance = ShellUpgradeService._();
|
||||||
ShellUpgradeService._();
|
ShellUpgradeService._();
|
||||||
|
|
||||||
String? _configUrl;
|
String? _configUrl;
|
||||||
|
Future<int?> Function()? _localBuildNumberResolver;
|
||||||
|
|
||||||
/// 注入升级配置地址。
|
/// 注入升级配置地址。
|
||||||
|
///
|
||||||
|
/// 这里只保存升级接口 URL,不会立即发起网络请求。
|
||||||
void setupConfigUrl(String? configUrl) {
|
void setupConfigUrl(String? configUrl) {
|
||||||
_configUrl = configUrl?.trim();
|
_configUrl = configUrl?.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查应用版本,若配置中有更新则会弹出升级弹窗。
|
/// 检查应用版本,若配置中有更新则会弹出升级弹窗。
|
||||||
|
///
|
||||||
|
/// 这里仅负责“拉取升级配置 + 触发升级插件比较”。
|
||||||
|
/// `initialUrl`、`preferredOrientations` 等运行时配置刷新,
|
||||||
|
/// 由 `core_app.dart` 中显式的 reload 方法单独处理。
|
||||||
Future<void> checkVersion(
|
Future<void> checkVersion(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
bool showNoUpdateToast = false,
|
bool showNoUpdateToast = false,
|
||||||
|
|
@ -63,7 +300,8 @@ class ShellUpgradeService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UpgradeAuxiliaryUtils.instance.initiateVersionCheck( // coverage:ignore-line
|
UpgradeAuxiliaryUtils.instance.initiateVersionCheck(
|
||||||
|
// coverage:ignore-line
|
||||||
context,
|
context,
|
||||||
showNoUpdateToast: showNoUpdateToast,
|
showNoUpdateToast: showNoUpdateToast,
|
||||||
future: _createVersionResolver(remoteConfig), // coverage:ignore-line
|
future: _createVersionResolver(remoteConfig), // coverage:ignore-line
|
||||||
|
|
@ -74,10 +312,53 @@ class ShellUpgradeService {
|
||||||
ShellUpgradeConfig remoteConfig,
|
ShellUpgradeConfig remoteConfig,
|
||||||
) {
|
) {
|
||||||
return (int upType) async {
|
return (int upType) async {
|
||||||
return _convertToAppUpgradeVersion(remoteConfig);
|
// 先把远端配置转换成升级插件需要的数据模型;
|
||||||
|
// 如果关键字段不完整,则直接中止本次升级检查。
|
||||||
|
final upgradeVersion = _convertToAppUpgradeVersion(remoteConfig);
|
||||||
|
if (upgradeVersion == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先用 WebShell 自己的版本比较逻辑做一次前置过滤,
|
||||||
|
// 避免远端版本不高时仍然进入插件弹窗流程。
|
||||||
|
// 若本地 buildNumber 读取失败,则回退给升级插件做内部比较。
|
||||||
|
final localBuildNumber = await _resolveLocalBuildNumber();
|
||||||
|
if (localBuildNumber == null) {
|
||||||
|
debugPrint('获取 WebShell 本地版本号失败,回退到升级插件内部版本比较');
|
||||||
|
return upgradeVersion;
|
||||||
|
}
|
||||||
|
if (!remoteConfig.shouldOfferUpgrade(localVersion: localBuildNumber)) {
|
||||||
|
debugPrint(
|
||||||
|
'WebShell 远端版本(${remoteConfig.version}) <= 本地版本($localBuildNumber),跳过升级提示',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return upgradeVersion;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 优先使用测试注入的 resolver;否则回退到升级插件读取本地 buildNumber。
|
||||||
|
Future<int?> _resolveLocalBuildNumber() async {
|
||||||
|
final resolver = _localBuildNumberResolver;
|
||||||
|
if (resolver != null) {
|
||||||
|
return resolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final appInfo = await AppUpgradePlugin().getAppInfo();
|
||||||
|
return int.tryParse((appInfo['buildNumber'] ?? '').trim());
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('获取 WebShell 本地版本号异常: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void debugSetLocalBuildNumberResolver(Future<int?> Function()? resolver) {
|
||||||
|
_localBuildNumberResolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从升级接口拉取原始 JSON,并交给统一解析器处理。
|
||||||
Future<ShellUpgradeConfig?> _fetchConfig(String url) async {
|
Future<ShellUpgradeConfig?> _fetchConfig(String url) async {
|
||||||
try {
|
try {
|
||||||
final uri = Uri.tryParse(url);
|
final uri = Uri.tryParse(url);
|
||||||
|
|
@ -98,14 +379,22 @@ class ShellUpgradeService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析接口返回字符串。
|
||||||
|
///
|
||||||
|
/// 只有当响应中至少成功解析出 `upgrade` 或 `config` 任一结构时,
|
||||||
|
/// 才认为这是有效的升级配置。
|
||||||
ShellUpgradeConfig? _parseConfigString(String content) {
|
ShellUpgradeConfig? _parseConfigString(String content) {
|
||||||
try {
|
try {
|
||||||
final dynamic jsonMap = jsonDecode(content);
|
final dynamic jsonMap = jsonDecode(content);
|
||||||
if (jsonMap is Map<String, dynamic>) {
|
if (jsonMap is Map<String, dynamic>) {
|
||||||
final data = jsonMap['data'] is Map<String, dynamic>
|
final config = ShellUpgradeConfig.fromJson(jsonMap);
|
||||||
? jsonMap['data'] as Map<String, dynamic>
|
return config.hasStructuredPayload ? config : null;
|
||||||
: jsonMap;
|
}
|
||||||
return ShellUpgradeConfig.fromJson(data);
|
if (jsonMap is Map) {
|
||||||
|
final config = ShellUpgradeConfig.fromJson(
|
||||||
|
Map<String, dynamic>.from(jsonMap),
|
||||||
|
);
|
||||||
|
return config.hasStructuredPayload ? config : null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('解析 WebShell 升级配置异常: $e');
|
debugPrint('解析 WebShell 升级配置异常: $e');
|
||||||
|
|
@ -114,6 +403,14 @@ class ShellUpgradeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将远程 JSON 配置转换为升级弹窗所需的数据模型。
|
/// 将远程 JSON 配置转换为升级弹窗所需的数据模型。
|
||||||
|
///
|
||||||
|
/// 这里会把:
|
||||||
|
/// - `upgrade.versionName` -> `versionName`
|
||||||
|
/// - `upgrade.version` -> `versionBuildNumber`
|
||||||
|
/// - `upgrade.isForce` -> `isForce`
|
||||||
|
/// - `describe/upgrade.remark` -> `updateContent`
|
||||||
|
/// - `installOssUrl/upgrade.filePath` -> 下载地址
|
||||||
|
/// - `upgrade.fileSize`(KB) -> `apkSize`(Byte)
|
||||||
AppUpgradeVersion? _convertToAppUpgradeVersion(ShellUpgradeConfig config) {
|
AppUpgradeVersion? _convertToAppUpgradeVersion(ShellUpgradeConfig config) {
|
||||||
if (config.version == null || config.versionName == null) {
|
if (config.version == null || config.versionName == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,22 @@ class ShellCoreTestHooks {
|
||||||
/// 执行启动配置合并流程。
|
/// 执行启动配置合并流程。
|
||||||
Future<String?> applyBootstrapConfig() => _applyBootstrapConfig();
|
Future<String?> applyBootstrapConfig() => _applyBootstrapConfig();
|
||||||
|
|
||||||
|
/// 重新拉取升级配置中的运行时启动配置并应用。
|
||||||
|
Future<ShellUpgradeConfig?> reloadUpgradeRuntimeConfigForTest() {
|
||||||
|
return reloadUpgradeRuntimeConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 先重载运行时升级配置,再执行升级检查。
|
||||||
|
Future<ShellUpgradeConfig?> reloadUpgradeRuntimeConfigAndCheckVersionForTest(
|
||||||
|
BuildContext context, {
|
||||||
|
bool showNoUpdateToast = false,
|
||||||
|
}) {
|
||||||
|
return reloadUpgradeRuntimeConfigAndCheckVersion(
|
||||||
|
context,
|
||||||
|
showNoUpdateToast: showNoUpdateToast,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 解析字符串格式的升级配置。
|
/// 解析字符串格式的升级配置。
|
||||||
ShellUpgradeConfig? parseUpgradeConfigString(String content) {
|
ShellUpgradeConfig? parseUpgradeConfigString(String content) {
|
||||||
return ShellUpgradeService.instance._parseConfigString(content);
|
return ShellUpgradeService.instance._parseConfigString(content);
|
||||||
|
|
@ -76,6 +92,11 @@ class ShellCoreTestHooks {
|
||||||
return ShellUpgradeService.instance._createVersionResolver(config)(upType);
|
return ShellUpgradeService.instance._createVersionResolver(config)(upType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 为测试注入本地构建号解析逻辑。
|
||||||
|
void setLocalBuildNumberResolver(Future<int?> Function()? resolver) {
|
||||||
|
ShellUpgradeService.instance.debugSetLocalBuildNumberResolver(resolver);
|
||||||
|
}
|
||||||
|
|
||||||
/// 为测试设置升级配置地址。
|
/// 为测试设置升级配置地址。
|
||||||
void setupUpgradeConfigUrl(String? url) {
|
void setupUpgradeConfigUrl(String? url) {
|
||||||
ShellUpgradeService.instance.setupConfigUrl(url);
|
ShellUpgradeService.instance.setupConfigUrl(url);
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,13 @@ class _WebShellPageState extends State<WebShellPage>
|
||||||
bool _hasStartedRemoteMainFrame = false;
|
bool _hasStartedRemoteMainFrame = false;
|
||||||
bool _hasMainFrameError = false;
|
bool _hasMainFrameError = false;
|
||||||
bool _hasMeasuredProgress = false;
|
bool _hasMeasuredProgress = false;
|
||||||
|
bool _isApplyingRuntimeConfigReload = false;
|
||||||
|
bool _hasPendingRuntimeConfigReload = false;
|
||||||
Timer? _startupWatchdogTimer;
|
Timer? _startupWatchdogTimer;
|
||||||
int _progress = 0;
|
int _progress = 0;
|
||||||
int _startupRetryCount = 0;
|
int _startupRetryCount = 0;
|
||||||
|
int _lastRuntimeConfigVersion = _runtimeConfigVersion.value;
|
||||||
|
String _appliedInitialUrl = _initialUrl;
|
||||||
String _currentUrl = _initialUrl;
|
String _currentUrl = _initialUrl;
|
||||||
String _errorTitle = '页面加载失败';
|
String _errorTitle = '页面加载失败';
|
||||||
String _errorMessage = '请检查网络后重试。';
|
String _errorMessage = '请检查网络后重试。';
|
||||||
|
|
@ -48,6 +52,7 @@ class _WebShellPageState extends State<WebShellPage>
|
||||||
super.initState();
|
super.initState();
|
||||||
debugPrint('WebShell 初始化,初始地址=$_initialUrl');
|
debugPrint('WebShell 初始化,初始地址=$_initialUrl');
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_runtimeConfigVersion.addListener(_handleRuntimeConfigChanged);
|
||||||
_androidCompatibilityFuture = _prepareAndroidCompatibility();
|
_androidCompatibilityFuture = _prepareAndroidCompatibility();
|
||||||
_recreateWebView();
|
_recreateWebView();
|
||||||
|
|
||||||
|
|
@ -57,8 +62,8 @@ class _WebShellPageState extends State<WebShellPage>
|
||||||
}
|
}
|
||||||
_hasTriggeredInitialLoad = true;
|
_hasTriggeredInitialLoad = true;
|
||||||
unawaited(_loadInitialPage());
|
unawaited(_loadInitialPage());
|
||||||
// 触发版本检测(如果配置了升级地址才会弹窗)
|
// 启动时先同步运行时配置,再执行升级检查。
|
||||||
unawaited(ShellUpgradeService.instance.checkVersion(context));
|
unawaited(reloadUpgradeRuntimeConfigAndCheckVersion(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -444,10 +449,51 @@ class _WebShellPageState extends State<WebShellPage>
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_cancelStartupWatchdog();
|
_cancelStartupWatchdog();
|
||||||
|
_runtimeConfigVersion.removeListener(_handleRuntimeConfigChanged);
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleRuntimeConfigChanged() {
|
||||||
|
final nextVersion = _runtimeConfigVersion.value;
|
||||||
|
if (nextVersion == _lastRuntimeConfigVersion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastRuntimeConfigVersion = nextVersion;
|
||||||
|
_hasPendingRuntimeConfigReload = true;
|
||||||
|
unawaited(_applyPendingRuntimeConfigReload());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _applyPendingRuntimeConfigReload() async {
|
||||||
|
if (_isApplyingRuntimeConfigReload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isApplyingRuntimeConfigReload = true;
|
||||||
|
try {
|
||||||
|
while (_hasPendingRuntimeConfigReload && mounted) {
|
||||||
|
_hasPendingRuntimeConfigReload = false;
|
||||||
|
|
||||||
|
final nextInitialUrl = _initialUrl;
|
||||||
|
final shouldReloadInitialPage = _appliedInitialUrl != nextInitialUrl;
|
||||||
|
_appliedInitialUrl = nextInitialUrl;
|
||||||
|
|
||||||
|
if (!shouldReloadInitialPage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('WebShell 检测到运行时配置更新,重新加载初始地址: $nextInitialUrl');
|
||||||
|
await _ensureCompatibilityPlanApplied();
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _startLoadSequence(rebuildWebView: false, resetRetryCount: true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_isApplyingRuntimeConfigReload = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 页面加载 ──
|
// ── 页面加载 ──
|
||||||
|
|
||||||
Future<void> _loadInitialPage() async {
|
Future<void> _loadInitialPage() async {
|
||||||
|
|
@ -693,6 +739,20 @@ class _WebShellPageState extends State<WebShellPage>
|
||||||
case 'reloadPage':
|
case 'reloadPage':
|
||||||
await _reloadPage();
|
await _reloadPage();
|
||||||
data = true;
|
data = true;
|
||||||
|
case 'reloadUpgradeRuntimeConfig':
|
||||||
|
data = _runtimeConfigSnapshotFromBridge(
|
||||||
|
await reloadUpgradeRuntimeConfig(),
|
||||||
|
);
|
||||||
|
case 'reloadUpgradeRuntimeConfigAndCheckVersion':
|
||||||
|
data = _runtimeConfigSnapshotFromBridge(
|
||||||
|
await reloadUpgradeRuntimeConfigAndCheckVersion(
|
||||||
|
context,
|
||||||
|
showNoUpdateToast: _boolValue(
|
||||||
|
payload['showNoUpdateToast'],
|
||||||
|
defaultValue: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
case 'goBack':
|
case 'goBack':
|
||||||
data = await _goBackFromBridge();
|
data = await _goBackFromBridge();
|
||||||
case 'closeApp':
|
case 'closeApp':
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ const MethodChannel _platformChannel = SystemChannels.platform;
|
||||||
const _permissionChannel = MethodChannel(
|
const _permissionChannel = MethodChannel(
|
||||||
'flutter.baseflow.com/permissions/methods',
|
'flutter.baseflow.com/permissions/methods',
|
||||||
);
|
);
|
||||||
|
const _packageInfoChannel = MethodChannel(
|
||||||
|
'dev.fluttercommunity.plus/package_info',
|
||||||
|
);
|
||||||
|
|
||||||
const _testEnvironment = ShellEnvironment(
|
const _testEnvironment = ShellEnvironment(
|
||||||
appName: '测试应用',
|
appName: '测试应用',
|
||||||
|
|
@ -47,8 +50,9 @@ Future<Uri> _startJsonServer(
|
||||||
);
|
);
|
||||||
request.response.write(body);
|
request.response.write(body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
request.response.statusCode =
|
request.response.statusCode = error is int
|
||||||
error is int ? error : HttpStatus.internalServerError;
|
? error
|
||||||
|
: HttpStatus.internalServerError;
|
||||||
} finally {
|
} finally {
|
||||||
await request.response.close();
|
await request.response.close();
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +124,19 @@ void main() {
|
||||||
for (final permission in permissions) permission: statusValue,
|
for (final permission in permissions) permission: statusValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(_packageInfoChannel, (call) async {
|
||||||
|
if (call.method != 'getAll') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <String, String>{
|
||||||
|
'appName': 'web_shell_core_test',
|
||||||
|
'packageName': 'com.example.web_shell_core_test',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'buildNumber': '1',
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
|
@ -128,6 +145,8 @@ void main() {
|
||||||
.setMockMethodCallHandler(_platformChannel, null);
|
.setMockMethodCallHandler(_platformChannel, null);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMethodCallHandler(_permissionChannel, null);
|
.setMockMethodCallHandler(_permissionChannel, null);
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(_packageInfoChannel, null);
|
||||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
.setMockMessageHandler('flutter/assets', null);
|
.setMockMessageHandler('flutter/assets', null);
|
||||||
});
|
});
|
||||||
|
|
@ -1254,7 +1273,10 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config, isNotNull);
|
expect(config, isNotNull);
|
||||||
expect(config!.bootstrapConfigUrl, ' https://example.com/bootstrap.json ');
|
expect(
|
||||||
|
config!.bootstrapConfigUrl,
|
||||||
|
' https://example.com/bootstrap.json ',
|
||||||
|
);
|
||||||
expect(config.upgradeConfigUrl, ' https://example.com/upgrade.json ');
|
expect(config.upgradeConfigUrl, ' https://example.com/upgrade.json ');
|
||||||
expect(
|
expect(
|
||||||
config.preferredOrientations,
|
config.preferredOrientations,
|
||||||
|
|
@ -1267,15 +1289,19 @@ void main() {
|
||||||
|
|
||||||
test('启动配置解析支持空数组、非法对象与非法 JSON', () {
|
test('启动配置解析支持空数组、非法对象与非法 JSON', () {
|
||||||
expect(
|
expect(
|
||||||
shellCoreTestHooks.parseBootstrapConfigString(
|
shellCoreTestHooks
|
||||||
'{"preferredOrientations":[]}',
|
.parseBootstrapConfigString(
|
||||||
)!.preferredOrientations,
|
'{"preferredOrientations":[]}',
|
||||||
|
)!
|
||||||
|
.preferredOrientations,
|
||||||
<DeviceOrientation>[],
|
<DeviceOrientation>[],
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shellCoreTestHooks.parseBootstrapConfigString(
|
shellCoreTestHooks
|
||||||
'{"preferredOrientations":"portraitUp"}',
|
.parseBootstrapConfigString(
|
||||||
)!.preferredOrientations,
|
'{"preferredOrientations":"portraitUp"}',
|
||||||
|
)!
|
||||||
|
.preferredOrientations,
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
expect(shellCoreTestHooks.parseBootstrapConfigString('['), isNull);
|
expect(shellCoreTestHooks.parseBootstrapConfigString('['), isNull);
|
||||||
|
|
@ -1319,9 +1345,12 @@ void main() {
|
||||||
await unavailableSocket.close();
|
await unavailableSocket.close();
|
||||||
|
|
||||||
final successUri = await _startJsonServer(
|
final successUri = await _startJsonServer(
|
||||||
(_) async => '{"data":{"initialUrl":"https://remote.example.com","upgradeConfigUrl":"https://remote.example.com/upgrade.json"}}',
|
(_) async =>
|
||||||
|
'{"data":{"initialUrl":"https://remote.example.com","upgradeConfigUrl":"https://remote.example.com/upgrade.json"}}',
|
||||||
|
);
|
||||||
|
final success = await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks.fetchBootstrapConfig(successUri.toString()),
|
||||||
);
|
);
|
||||||
final success = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchBootstrapConfig(successUri.toString()));
|
|
||||||
expect(success?.initialUrl, 'https://remote.example.com');
|
expect(success?.initialUrl, 'https://remote.example.com');
|
||||||
expect(
|
expect(
|
||||||
success?.upgradeConfigUrl,
|
success?.upgradeConfigUrl,
|
||||||
|
|
@ -1341,7 +1370,9 @@ void main() {
|
||||||
expect(invalidUri?.initialUrl, 'https://remote.example.com');
|
expect(invalidUri?.initialUrl, 'https://remote.example.com');
|
||||||
|
|
||||||
final notFoundUri = await _startJsonServer((_) async => throw 404);
|
final notFoundUri = await _startJsonServer((_) async => throw 404);
|
||||||
final notFound = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchBootstrapConfig(notFoundUri.toString()));
|
final notFound = await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks.fetchBootstrapConfig(notFoundUri.toString()),
|
||||||
|
);
|
||||||
expect(notFound?.initialUrl, 'https://remote.example.com');
|
expect(notFound?.initialUrl, 'https://remote.example.com');
|
||||||
|
|
||||||
final failed = await _runWithRealHttpClient(
|
final failed = await _runWithRealHttpClient(
|
||||||
|
|
@ -1355,7 +1386,9 @@ void main() {
|
||||||
test('读取启动配置缓存异常时返回 null', () async {
|
test('读取启动配置缓存异常时返回 null', () async {
|
||||||
final originalStore = SharedPreferencesStorePlatform.instance;
|
final originalStore = SharedPreferencesStorePlatform.instance;
|
||||||
SharedPreferencesStorePlatform.instance = _ThrowingPreferencesStore();
|
SharedPreferencesStorePlatform.instance = _ThrowingPreferencesStore();
|
||||||
addTearDown(() => SharedPreferencesStorePlatform.instance = originalStore);
|
addTearDown(
|
||||||
|
() => SharedPreferencesStorePlatform.instance = originalStore,
|
||||||
|
);
|
||||||
|
|
||||||
expect(await shellCoreTestHooks.loadCachedBootstrapConfig(), isNull);
|
expect(await shellCoreTestHooks.loadCachedBootstrapConfig(), isNull);
|
||||||
});
|
});
|
||||||
|
|
@ -1379,7 +1412,7 @@ void main() {
|
||||||
expect(script, contains('window.AppShell'));
|
expect(script, contains('window.AppShell'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('脚本暴露全部 12 个 Action', () {
|
test('脚本暴露全部 14 个 Action', () {
|
||||||
final script = shellCoreTestHooks.buildAppShellBridgeScript();
|
final script = shellCoreTestHooks.buildAppShellBridgeScript();
|
||||||
|
|
||||||
for (final action in <String>[
|
for (final action in <String>[
|
||||||
|
|
@ -1389,6 +1422,8 @@ void main() {
|
||||||
'openExternal',
|
'openExternal',
|
||||||
'requestPermissions',
|
'requestPermissions',
|
||||||
'reloadPage',
|
'reloadPage',
|
||||||
|
'reloadUpgradeRuntimeConfig',
|
||||||
|
'reloadUpgradeRuntimeConfigAndCheckVersion',
|
||||||
'goBack',
|
'goBack',
|
||||||
'closeApp',
|
'closeApp',
|
||||||
'getDeviceInfo',
|
'getDeviceInfo',
|
||||||
|
|
@ -1603,6 +1638,7 @@ void main() {
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Phase 2: 新增 Bridge Action', () {
|
group('Phase 2: 新增 Bridge Action', () {
|
||||||
|
|
@ -1943,41 +1979,309 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('升级配置解析支持 data、平铺、isforce 和非法 JSON', () {
|
testWidgets('checkVersion 不会隐式覆盖运行时首页配置', (tester) async {
|
||||||
final wrapped = shellCoreTestHooks.parseUpgradeConfigString(
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
'{"data":{"versionName":"1.0.1","version":101,"isforce":1,"remark":"fix","filePath":"https://example.com/app.apk","fileSize":2048}}',
|
_testEnvironment.copyWith(
|
||||||
|
initialUrl: 'https://before.example.com/home',
|
||||||
|
preferredOrientations: <DeviceOrientation>[
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final flat = shellCoreTestHooks.parseUpgradeConfigString(
|
addTearDown(
|
||||||
'{"versionName":"1.0.2","version":102}',
|
() => shellCoreTestHooks.initializeEnvironment(_testEnvironment),
|
||||||
|
);
|
||||||
|
late BuildContext pageContext;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
pageContext = context;
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapped, isNotNull);
|
await tester.runAsync(() async {
|
||||||
expect(wrapped!.versionName, '1.0.1');
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
expect(wrapped.isForce, 1);
|
try {
|
||||||
expect(flat?.version, 102);
|
server.listen((request) async {
|
||||||
expect(shellCoreTestHooks.parseUpgradeConfigString('{'), isNull);
|
request.response.statusCode = HttpStatus.ok;
|
||||||
|
request.response.headers.set(
|
||||||
|
HttpHeaders.contentTypeHeader,
|
||||||
|
'application/json; charset=utf-8',
|
||||||
|
);
|
||||||
|
request.response.write(
|
||||||
|
jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'data': {
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '1.0.0',
|
||||||
|
'version': 1,
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'initialUrl': 'https://remote.example.com/runtime',
|
||||||
|
'preferredOrientations': ['portraitUp', 'portraitDown'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await request.response.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
shellCoreTestHooks.setupUpgradeConfigUrl(
|
||||||
|
'http://127.0.0.1:${server.port}/config.json',
|
||||||
|
);
|
||||||
|
await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks.checkVersion(pageContext),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await server.close(force: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.configuredInitialUrl,
|
||||||
|
'https://before.example.com/home',
|
||||||
|
);
|
||||||
|
expect(shellCoreTestHooks.initialUrl, 'https://before.example.com/home');
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.configuredPreferredOrientations,
|
||||||
|
<DeviceOrientation>[DeviceOrientation.landscapeLeft],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('升级配置转换为 AppUpgradeVersion 时处理缺省值', () {
|
testWidgets('reloadUpgradeRuntimeConfigAndCheckVersion 先重载配置再检查升级', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
|
_testEnvironment.copyWith(
|
||||||
|
initialUrl: 'https://before.example.com/home',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addTearDown(() {
|
||||||
|
shellCoreTestHooks.initializeEnvironment(_testEnvironment);
|
||||||
|
shellCoreTestHooks.setupUpgradeConfigUrl(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
late BuildContext pageContext;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
pageContext = context;
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final upgradeServer = await HttpServer.bind(
|
||||||
|
InternetAddress.loopbackIPv4,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
var upgradeCheckCount = 0;
|
||||||
|
upgradeServer.listen((request) async {
|
||||||
|
upgradeCheckCount += 1;
|
||||||
|
request.response.statusCode = HttpStatus.ok;
|
||||||
|
request.response.headers.set(
|
||||||
|
HttpHeaders.contentTypeHeader,
|
||||||
|
'application/json; charset=utf-8',
|
||||||
|
);
|
||||||
|
request.response.write(
|
||||||
|
jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'data': {
|
||||||
|
'installOssUrl': 'https://example.com/app.apk',
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '1.0.0',
|
||||||
|
'version': 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await request.response.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
final configServer = await HttpServer.bind(
|
||||||
|
InternetAddress.loopbackIPv4,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
configServer.listen((request) async {
|
||||||
|
request.response.statusCode = HttpStatus.ok;
|
||||||
|
request.response.headers.set(
|
||||||
|
HttpHeaders.contentTypeHeader,
|
||||||
|
'application/json; charset=utf-8',
|
||||||
|
);
|
||||||
|
request.response.write(
|
||||||
|
jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'data': {
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '1.0.0',
|
||||||
|
'version': 1,
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'initialUrl': 'https://remote.example.com/runtime',
|
||||||
|
'preferredOrientations': ['portraitUp', 'portraitDown'],
|
||||||
|
'upgradeConfigUrl':
|
||||||
|
'http://127.0.0.1:${upgradeServer.port}/upgrade.json',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await request.response.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
shellCoreTestHooks.setupUpgradeConfigUrl(
|
||||||
|
'http://127.0.0.1:${configServer.port}/config.json',
|
||||||
|
);
|
||||||
|
final remoteConfig = await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks
|
||||||
|
.reloadUpgradeRuntimeConfigAndCheckVersionForTest(
|
||||||
|
pageContext,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(remoteConfig, isNotNull);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.configuredInitialUrl,
|
||||||
|
'https://remote.example.com/runtime',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.upgradeConfigUrl,
|
||||||
|
'http://127.0.0.1:${upgradeServer.port}/upgrade.json',
|
||||||
|
);
|
||||||
|
expect(upgradeCheckCount, 1);
|
||||||
|
} finally {
|
||||||
|
await configServer.close(force: true);
|
||||||
|
await upgradeServer.close(force: true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('升级配置解析仅支持新版接口响应', () {
|
||||||
|
final wrappedContent = jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'code': null,
|
||||||
|
'msg': null,
|
||||||
|
'data': {
|
||||||
|
'id': 787171620319301,
|
||||||
|
'upgradeFileId': 0,
|
||||||
|
'installFileId': 787171620307013,
|
||||||
|
'isLatest': true,
|
||||||
|
'appId': 787154824761413,
|
||||||
|
'code': '1.0.1',
|
||||||
|
'releaseTime': '2026-03-24 09:51:23',
|
||||||
|
'releaseStatus': true,
|
||||||
|
'upgradeOssUrl': '',
|
||||||
|
'installOssUrl':
|
||||||
|
'https://umsapi.23544.com/api/biz/Action/download-app?appId=787154824761413&environment=DevelopmentEnvironment&fileType=install&versionCode=1.0.1',
|
||||||
|
'isAllowRollback': true,
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '1.0.0',
|
||||||
|
'version': 100,
|
||||||
|
'isForce': 0,
|
||||||
|
'remark': '1. 修复已知问题\n2. 测试升级弹窗功能是否正常加载',
|
||||||
|
'filePath':
|
||||||
|
'https://gitee.com/mr_koi/static_host/raw/master/app-release.apk',
|
||||||
|
'fileSize': 30000,
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'initialUrl': 'http://xszy.lzzneng.com/login.html',
|
||||||
|
'preferredOrientations': ['portraitUp', 'portraitDown'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'configDescribe': '',
|
||||||
|
'describe': '首次更新',
|
||||||
|
'describeType': 1,
|
||||||
|
'environment': 1,
|
||||||
|
'createdTime': '2026-03-24 09:49:32.581',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
final structured = shellCoreTestHooks.parseUpgradeConfigString(
|
||||||
|
wrappedContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(structured, isNotNull);
|
||||||
|
expect(structured!.success, isTrue);
|
||||||
|
expect(structured.id, 787171620319301);
|
||||||
|
expect(structured.installFileId, 787171620307013);
|
||||||
|
expect(structured.isLatest, isTrue);
|
||||||
|
expect(structured.appId, 787154824761413);
|
||||||
|
expect(structured.releaseCode, '1.0.1');
|
||||||
|
expect(structured.releaseStatus, isTrue);
|
||||||
|
expect(structured.isAllowRollback, isTrue);
|
||||||
|
expect(structured.describe, '首次更新');
|
||||||
|
expect(structured.describeType, 1);
|
||||||
|
expect(structured.environment, 1);
|
||||||
|
expect(structured.createdTime, '2026-03-24 09:49:32.581');
|
||||||
|
expect(structured.upgrade, isNotNull);
|
||||||
|
expect(structured.versionName, '1.0.0');
|
||||||
|
expect(structured.version, 100);
|
||||||
|
expect(structured.isForce, 0);
|
||||||
|
expect(structured.remark, contains('修复已知问题'));
|
||||||
expect(
|
expect(
|
||||||
shellCoreTestHooks.convertUpgradeConfig(
|
structured.filePath,
|
||||||
ShellUpgradeConfig(versionName: '1.0.0'),
|
'https://umsapi.23544.com/api/biz/Action/download-app?appId=787154824761413&environment=DevelopmentEnvironment&fileType=install&versionCode=1.0.1',
|
||||||
|
);
|
||||||
|
expect(structured.fileSize, 30000);
|
||||||
|
expect(structured.config, isNotNull);
|
||||||
|
expect(
|
||||||
|
structured.config?.initialUrl,
|
||||||
|
'http://xszy.lzzneng.com/login.html',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
structured.config?.preferredOrientations,
|
||||||
|
<DeviceOrientation>[
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.parseUpgradeConfigString(
|
||||||
|
'{"versionName":"1.0.2","version":102}',
|
||||||
),
|
),
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
|
expect(shellCoreTestHooks.parseUpgradeConfigString('{'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('升级配置转换为 AppUpgradeVersion 时仅支持顶层 upgrade', () {
|
||||||
expect(
|
expect(
|
||||||
shellCoreTestHooks.convertUpgradeConfig(ShellUpgradeConfig(version: 1)),
|
shellCoreTestHooks.convertUpgradeConfig(const ShellUpgradeConfig()),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.convertUpgradeConfig(
|
||||||
|
const ShellUpgradeConfig(
|
||||||
|
upgrade: ShellUpgradeReleaseConfig(version: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
|
|
||||||
final converted = shellCoreTestHooks.convertUpgradeConfig(
|
final converted = shellCoreTestHooks.convertUpgradeConfig(
|
||||||
ShellUpgradeConfig(
|
const ShellUpgradeConfig(
|
||||||
versionName: '1.2.3',
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
version: 123,
|
versionName: '1.2.3',
|
||||||
isForce: 1,
|
version: 123,
|
||||||
remark: 'important update',
|
isForce: 1,
|
||||||
filePath: '',
|
remark: 'important update',
|
||||||
fileSize: 2048,
|
fileSize: 2048,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1992,29 +2296,74 @@ void main() {
|
||||||
expect(converted.supportedMethods, hasLength(3));
|
expect(converted.supportedMethods, hasLength(3));
|
||||||
|
|
||||||
final convertedWithPath = shellCoreTestHooks.convertUpgradeConfig(
|
final convertedWithPath = shellCoreTestHooks.convertUpgradeConfig(
|
||||||
ShellUpgradeConfig(
|
const ShellUpgradeConfig(
|
||||||
versionName: '2.0.0',
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
version: 200,
|
versionName: '2.0.0',
|
||||||
filePath: 'https://example.com/app.apk',
|
version: 200,
|
||||||
|
filePath: 'https://example.com/app.apk',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(convertedWithPath?.downloadUrl, 'https://example.com/app.apk');
|
expect(convertedWithPath?.downloadUrl, 'https://example.com/app.apk');
|
||||||
expect(convertedWithPath?.appStoreUrl, 'https://example.com/app.apk');
|
expect(convertedWithPath?.appStoreUrl, 'https://example.com/app.apk');
|
||||||
|
|
||||||
|
final convertedFromStructuredConfig = shellCoreTestHooks
|
||||||
|
.convertUpgradeConfig(
|
||||||
|
const ShellUpgradeConfig(
|
||||||
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
|
versionName: '3.0.0',
|
||||||
|
version: 300,
|
||||||
|
isForce: 0,
|
||||||
|
remark: '1. 修复已知问题\n2. 测试升级弹窗功能是否正常加载',
|
||||||
|
filePath: 'https://download.example.com/app.apk',
|
||||||
|
fileSize: 25000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(convertedFromStructuredConfig, isNotNull);
|
||||||
|
expect(convertedFromStructuredConfig!.versionName, '3.0.0');
|
||||||
|
expect(convertedFromStructuredConfig.versionBuildNumber, 300);
|
||||||
|
expect(convertedFromStructuredConfig.isForce, isFalse);
|
||||||
|
expect(convertedFromStructuredConfig.updateContent, contains('修复已知问题'));
|
||||||
|
expect(
|
||||||
|
convertedFromStructuredConfig.downloadUrl,
|
||||||
|
'https://download.example.com/app.apk',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
convertedFromStructuredConfig.appStoreUrl,
|
||||||
|
'https://download.example.com/app.apk',
|
||||||
|
);
|
||||||
|
expect(convertedFromStructuredConfig.apkSize, 25000 * 1024);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('获取升级配置支持成功、非法地址、非 200 和异常', () async {
|
test('获取升级配置支持成功、非法地址、非 200 和异常', () async {
|
||||||
final successUri = await _startJsonServer(
|
final successUri = await _startJsonServer(
|
||||||
(_) async => '{"versionName":"2.0.0","version":200}',
|
(_) async => jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'data': {
|
||||||
|
'installOssUrl': 'https://example.com/app.apk',
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'version': 200,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
final success = await _runWithRealHttpClient(() => shellCoreTestHooks.fetchUpgradeConfig(successUri.toString()));
|
final success = await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks.fetchUpgradeConfig(successUri.toString()),
|
||||||
|
);
|
||||||
expect(success?.versionName, '2.0.0');
|
expect(success?.versionName, '2.0.0');
|
||||||
|
|
||||||
expect(await shellCoreTestHooks.fetchUpgradeConfig('%%%'), isNull);
|
expect(await shellCoreTestHooks.fetchUpgradeConfig('%%%'), isNull);
|
||||||
|
|
||||||
final notFoundUri = await _startJsonServer((_) async => throw 404);
|
final notFoundUri = await _startJsonServer((_) async => throw 404);
|
||||||
expect(
|
expect(
|
||||||
await _runWithRealHttpClient(() => shellCoreTestHooks.fetchUpgradeConfig(notFoundUri.toString())),
|
await _runWithRealHttpClient(
|
||||||
|
() => shellCoreTestHooks.fetchUpgradeConfig(notFoundUri.toString()),
|
||||||
|
),
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2037,7 +2386,8 @@ void main() {
|
||||||
|
|
||||||
test('applyBootstrapConfig 会用远程启动配置覆盖本地配置', () async {
|
test('applyBootstrapConfig 会用远程启动配置覆盖本地配置', () async {
|
||||||
final bootstrapUri = await _startJsonServer(
|
final bootstrapUri = await _startJsonServer(
|
||||||
(_) async => '{"data":{"initialUrl":"https://remote.example.com/home","preferredOrientations":["portraitDown"],"upgradeConfigUrl":" https://remote.example.com/upgrade.json "}}',
|
(_) async =>
|
||||||
|
'{"data":{"initialUrl":"https://remote.example.com/home","preferredOrientations":["portraitDown"],"upgradeConfigUrl":" https://remote.example.com/upgrade.json "}}',
|
||||||
);
|
);
|
||||||
shellCoreTestHooks.initializeEnvironment(
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
const ShellEnvironment(
|
const ShellEnvironment(
|
||||||
|
|
@ -2096,8 +2446,7 @@ void main() {
|
||||||
assetContents['assets/config/fallback_bootstrap.json'] = jsonEncode(
|
assetContents['assets/config/fallback_bootstrap.json'] = jsonEncode(
|
||||||
<String, Object?>{
|
<String, Object?>{
|
||||||
'initialUrl': 'https://asset.example.com/start',
|
'initialUrl': 'https://asset.example.com/start',
|
||||||
'bootstrapConfigUrl':
|
'bootstrapConfigUrl': 'http://127.0.0.1:$unavailablePort/config.json',
|
||||||
'http://127.0.0.1:$unavailablePort/config.json',
|
|
||||||
'upgradeConfigUrl': ' https://asset.example.com/upgrade.json ',
|
'upgradeConfigUrl': ' https://asset.example.com/upgrade.json ',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -2128,13 +2477,82 @@ void main() {
|
||||||
shellCoreTestHooks.initializeEnvironment(_testEnvironment);
|
shellCoreTestHooks.initializeEnvironment(_testEnvironment);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('reloadUpgradeRuntimeConfig 会应用升级配置中的运行时启动配置', () async {
|
||||||
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
|
_testEnvironment.copyWith(
|
||||||
|
initialUrl: 'https://before.example.com/home',
|
||||||
|
preferredOrientations: <DeviceOrientation>[
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addTearDown(() {
|
||||||
|
shellCoreTestHooks.initializeEnvironment(_testEnvironment);
|
||||||
|
shellCoreTestHooks.setupUpgradeConfigUrl(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
final successUri = await _startJsonServer(
|
||||||
|
(_) async => jsonEncode({
|
||||||
|
'success': true,
|
||||||
|
'data': {
|
||||||
|
'config': jsonEncode({
|
||||||
|
'upgrade': {
|
||||||
|
'versionName': '2.0.0',
|
||||||
|
'version': 200,
|
||||||
|
},
|
||||||
|
'config': {
|
||||||
|
'initialUrl': 'https://remote.example.com/runtime',
|
||||||
|
'preferredOrientations': ['portraitUp', 'portraitDown'],
|
||||||
|
'upgradeConfigUrl': 'https://remote.example.com/upgrade.json',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
shellCoreTestHooks.setupUpgradeConfigUrl(successUri.toString());
|
||||||
|
|
||||||
|
final remoteConfig = await _runWithRealHttpClient(
|
||||||
|
shellCoreTestHooks.reloadUpgradeRuntimeConfigForTest,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(remoteConfig, isNotNull);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.configuredInitialUrl,
|
||||||
|
'https://remote.example.com/runtime',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.initialUrl,
|
||||||
|
'https://remote.example.com/runtime',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.configuredPreferredOrientations,
|
||||||
|
<DeviceOrientation>[
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.preferredOrientations,
|
||||||
|
<DeviceOrientation>[
|
||||||
|
DeviceOrientation.portraitUp,
|
||||||
|
DeviceOrientation.portraitDown,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
shellCoreTestHooks.upgradeConfigUrl,
|
||||||
|
'https://remote.example.com/upgrade.json',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('LaunchOverlay 配置品牌图片时渲染 Image', (tester) async {
|
testWidgets('LaunchOverlay 配置品牌图片时渲染 Image', (tester) async {
|
||||||
shellCoreTestHooks.initializeEnvironment(
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
_testEnvironment.copyWith(
|
_testEnvironment.copyWith(
|
||||||
splashImage: MemoryImage(Uint8List.fromList(kTransparentImage)),
|
splashImage: MemoryImage(Uint8List.fromList(kTransparentImage)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
addTearDown(() => shellCoreTestHooks.initializeEnvironment(_testEnvironment));
|
addTearDown(
|
||||||
|
() => shellCoreTestHooks.initializeEnvironment(_testEnvironment),
|
||||||
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
|
|
@ -2213,18 +2631,53 @@ void main() {
|
||||||
expect(find.text('长提示'), findsNothing);
|
expect(find.text('长提示'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('升级配置异步解析闭包可返回版本信息', () async {
|
test('升级配置异步解析闭包按本地版本决定是否返回升级信息', () async {
|
||||||
|
addTearDown(() => shellCoreTestHooks.setLocalBuildNumberResolver(null));
|
||||||
|
|
||||||
|
shellCoreTestHooks.setLocalBuildNumberResolver(() async => 200);
|
||||||
final version = await shellCoreTestHooks.resolveUpgradeConfig(
|
final version = await shellCoreTestHooks.resolveUpgradeConfig(
|
||||||
ShellUpgradeConfig(
|
const ShellUpgradeConfig(
|
||||||
versionName: '3.0.0',
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
version: 300,
|
versionName: '3.0.0',
|
||||||
filePath: 'https://example.com/app.apk',
|
version: 300,
|
||||||
|
filePath: 'https://example.com/app.apk',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(version?.versionName, '3.0.0');
|
expect(version?.versionName, '3.0.0');
|
||||||
expect(version?.versionBuildNumber, 300);
|
expect(version?.versionBuildNumber, 300);
|
||||||
expect(version?.downloadUrl, 'https://example.com/app.apk');
|
expect(version?.downloadUrl, 'https://example.com/app.apk');
|
||||||
|
|
||||||
|
shellCoreTestHooks.setLocalBuildNumberResolver(() async => 300);
|
||||||
|
final sameVersion = await shellCoreTestHooks.resolveUpgradeConfig(
|
||||||
|
const ShellUpgradeConfig(
|
||||||
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
|
versionName: '3.0.0',
|
||||||
|
version: 300,
|
||||||
|
filePath: 'https://example.com/app.apk',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(sameVersion, isNull);
|
||||||
|
|
||||||
|
shellCoreTestHooks.setLocalBuildNumberResolver(() async => null);
|
||||||
|
final fallbackVersion = await shellCoreTestHooks.resolveUpgradeConfig(
|
||||||
|
const ShellUpgradeConfig(
|
||||||
|
upgrade: ShellUpgradeReleaseConfig(
|
||||||
|
versionName: '3.0.0',
|
||||||
|
version: 300,
|
||||||
|
filePath: 'https://example.com/app.apk',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(fallbackVersion?.versionName, '3.0.0');
|
||||||
|
expect(fallbackVersion?.versionBuildNumber, 300);
|
||||||
|
|
||||||
|
final hiddenVersion = await shellCoreTestHooks.resolveUpgradeConfig(
|
||||||
|
const ShellUpgradeConfig(),
|
||||||
|
);
|
||||||
|
expect(hiddenVersion, isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2467,6 +2920,7 @@ class _FakePlatformWebViewController extends PlatformWebViewController {
|
||||||
_FakePlatformWebViewController(super.params) : super.implementation();
|
_FakePlatformWebViewController(super.params) : super.implementation();
|
||||||
|
|
||||||
final javaScriptCalls = <String>[];
|
final javaScriptCalls = <String>[];
|
||||||
|
final javaScriptChannels = <String, JavaScriptChannelParams>{};
|
||||||
PlatformNavigationDelegate? delegate;
|
PlatformNavigationDelegate? delegate;
|
||||||
bool throwOnRunJavaScript = false;
|
bool throwOnRunJavaScript = false;
|
||||||
bool canGoBackValue = false;
|
bool canGoBackValue = false;
|
||||||
|
|
@ -2474,7 +2928,20 @@ class _FakePlatformWebViewController extends PlatformWebViewController {
|
||||||
Uri? lastLoadedUri;
|
Uri? lastLoadedUri;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> addJavaScriptChannel(JavaScriptChannelParams params) async {}
|
Future<void> addJavaScriptChannel(JavaScriptChannelParams params) async {
|
||||||
|
javaScriptChannels[params.name] = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispatchJavaScriptMessage(
|
||||||
|
String channelName,
|
||||||
|
String message,
|
||||||
|
) async {
|
||||||
|
final channel = javaScriptChannels[channelName];
|
||||||
|
if (channel == null) {
|
||||||
|
throw StateError('JavaScript channel not found: $channelName');
|
||||||
|
}
|
||||||
|
channel.onMessageReceived(JavaScriptMessage(message: message));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> clearCache() async {}
|
Future<void> clearCache() async {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue