feat:加载远程配置
This commit is contained in:
parent
44cf59749d
commit
c08d5b58eb
|
|
@ -55,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;
|
||||||
|
|
@ -84,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;
|
||||||
|
|
@ -146,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'),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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: '测试应用',
|
||||||
|
|
@ -121,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(() {
|
||||||
|
|
@ -129,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);
|
||||||
});
|
});
|
||||||
|
|
@ -1394,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>[
|
||||||
|
|
@ -1404,6 +1422,8 @@ void main() {
|
||||||
'openExternal',
|
'openExternal',
|
||||||
'requestPermissions',
|
'requestPermissions',
|
||||||
'reloadPage',
|
'reloadPage',
|
||||||
|
'reloadUpgradeRuntimeConfig',
|
||||||
|
'reloadUpgradeRuntimeConfigAndCheckVersion',
|
||||||
'goBack',
|
'goBack',
|
||||||
'closeApp',
|
'closeApp',
|
||||||
'getDeviceInfo',
|
'getDeviceInfo',
|
||||||
|
|
@ -1618,6 +1638,7 @@ void main() {
|
||||||
isNull,
|
isNull,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('Phase 2: 新增 Bridge Action', () {
|
group('Phase 2: 新增 Bridge Action', () {
|
||||||
|
|
@ -1958,6 +1979,197 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('checkVersion 不会隐式覆盖运行时首页配置', (tester) async {
|
||||||
|
shellCoreTestHooks.initializeEnvironment(
|
||||||
|
_testEnvironment.copyWith(
|
||||||
|
initialUrl: 'https://before.example.com/home',
|
||||||
|
preferredOrientations: <DeviceOrientation>[
|
||||||
|
DeviceOrientation.landscapeLeft,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addTearDown(
|
||||||
|
() => shellCoreTestHooks.initializeEnvironment(_testEnvironment),
|
||||||
|
);
|
||||||
|
late BuildContext pageContext;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
pageContext = context;
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
try {
|
||||||
|
server.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'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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('升级配置解析仅支持新版接口响应', () {
|
test('升级配置解析仅支持新版接口响应', () {
|
||||||
final wrappedContent = jsonEncode({
|
final wrappedContent = jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
|
|
@ -2265,6 +2477,73 @@ 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(
|
||||||
|
|
@ -2641,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;
|
||||||
|
|
@ -2648,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