From ab74bcd5c2ad273845997ab494442ca21381e934 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 5 Jun 2026 10:06:43 +0000 Subject: [PATCH] test(ensemble): cover CDN pending updates and upload batch splitting - Extract splitUploadFileBatches for testable background upload batching - Add tests for CDN _handlePendingUpdate app bundle sync and resume path - Expose minimal @visibleForTesting hooks on CdnDefinitionProvider Co-authored-by: Sharjeel Yunus --- .../lib/action/upload_files_action.dart | 14 +-- .../definition_providers/cdn_provider.dart | 14 +++ modules/ensemble/lib/util/upload_utils.dart | 15 +++ modules/ensemble/test/cdn_provider_test.dart | 95 +++++++++++++++++++ .../test/upload_batch_split_test.dart | 36 +++++++ 5 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 modules/ensemble/test/upload_batch_split_test.dart diff --git a/modules/ensemble/lib/action/upload_files_action.dart b/modules/ensemble/lib/action/upload_files_action.dart index 6e3be552d..2a2c3384b 100644 --- a/modules/ensemble/lib/action/upload_files_action.dart +++ b/modules/ensemble/lib/action/upload_files_action.dart @@ -97,18 +97,8 @@ Future uploadFiles({ : scopeManager?.dataContext.getContextById(action.id!) as UploadFilesResponse; - List> fileBatches; - if (action.batchSize != null) { - fileBatches = []; - for (int i = 0; i < selectedFiles.length; i += action.batchSize!) { - int end = (i + action.batchSize! < selectedFiles.length) - ? i + action.batchSize! - : selectedFiles.length; - fileBatches.add(selectedFiles.sublist(i, end)); - } - } else { - fileBatches = [selectedFiles]; - } + final fileBatches = + splitUploadFileBatches(selectedFiles, action.batchSize); for (var fileBatch in fileBatches) { if (action.isBackgroundTask) { diff --git a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart index 5ce6c2e7d..1e955bd97 100644 --- a/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/cdn_provider.dart @@ -922,6 +922,20 @@ class CdnDefinitionProvider extends DefinitionProvider { await _refreshTranslationsAtRuntime(); } + @visibleForTesting + Future handlePendingUpdateForTesting() => _handlePendingUpdate(); + + @visibleForTesting + bool get hasPendingUpdateForTesting => _hasPendingUpdate; + + @visibleForTesting + set hasPendingUpdateForTesting(bool value) => _hasPendingUpdate = value; + + @visibleForTesting + void rebuildManifestCacheForTesting(Map root) { + _rebuildFromRoot(root); + } + @visibleForTesting Future loadCachedStateForTesting() => _loadCachedState(); diff --git a/modules/ensemble/lib/util/upload_utils.dart b/modules/ensemble/lib/util/upload_utils.dart index 864c7585b..4fc36b472 100644 --- a/modules/ensemble/lib/util/upload_utils.dart +++ b/modules/ensemble/lib/util/upload_utils.dart @@ -27,6 +27,21 @@ bool uploadPathContainsParentSegment(String? path) { return false; } +/// Splits [files] into upload batches of at most [batchSize] items. +/// +/// When [batchSize] is null, returns a single batch containing all [files]. +List> splitUploadFileBatches(List files, int? batchSize) { + if (batchSize == null) { + return [files]; + } + final batches = >[]; + for (var i = 0; i < files.length; i += batchSize) { + final end = i + batchSize < files.length ? i + batchSize : files.length; + batches.add(files.sublist(i, end)); + } + return batches; +} + int getInt(String id) { return id.codeUnits.reduce((a, b) => a + b); } diff --git a/modules/ensemble/test/cdn_provider_test.dart b/modules/ensemble/test/cdn_provider_test.dart index 97d3df78d..8178cbe19 100644 --- a/modules/ensemble/test/cdn_provider_test.dart +++ b/modules/ensemble/test/cdn_provider_test.dart @@ -1,6 +1,8 @@ import 'dart:ui'; +import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/definition_providers/cdn_provider.dart'; +import 'package:ensemble/framework/definition_providers/provider.dart'; import 'package:ensemble/util/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -111,6 +113,40 @@ void main() { expect(find.text('Hello from default EN'), findsOneWidget); }); + testWidgets('applies pending translation updates on app resume', + (tester) async { + final provider = CdnDefinitionProvider('test-app'); + final config = EnsembleConfig(definitionProvider: provider); + Ensemble().setEnsembleConfig(config); + + await provider.applyRuntimeManifestForTesting( + _manifestWithArtifactRefresh(_manifestWithoutNewKey()), + ); + await config.updateAppBundle(); + + final tick = await _pumpTranslationApp( + tester, + provider: provider, + locale: const Locale('en'), + translationKey: 'greeting.new', + ); + await tester.pumpAndSettle(); + expect(find.text('__missing__'), findsOneWidget); + + provider.rebuildManifestCacheForTesting( + _manifestWithArtifactRefresh(_manifestWithNewKey()), + ); + provider.hasPendingUpdateForTesting = true; + + provider.onAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pumpAndSettle(); + tick.value++; + await tester.pumpAndSettle(); + + expect(find.text('Hello from CDN'), findsOneWidget); + expect(provider.hasPendingUpdateForTesting, isFalse); + }); + testWidgets('updates changed value for existing translation key', (tester) async { final provider = CdnDefinitionProvider('test-app'); @@ -134,8 +170,67 @@ void main() { expect(find.text('Hello updated'), findsOneWidget); }); }); + + group('CDN pending update ordering', () { + test('handlePendingUpdate syncs app bundle from CDN cache and fires refresh', + () async { + final provider = CdnDefinitionProvider('test-app'); + final config = EnsembleConfig(definitionProvider: provider); + Ensemble().setEnsembleConfig(config); + + await provider.applyRuntimeManifestForTesting( + _manifestWithResourceVersion('v1'), + ); + await config.updateAppBundle(); + + expect( + config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'], + 'v1', + ); + + provider.rebuildManifestCacheForTesting( + _manifestWithResourceVersion('v2'), + ); + provider.hasPendingUpdateForTesting = true; + + await provider.handlePendingUpdateForTesting(); + + expect( + config.getResources()?[ResourceArtifactEntry.Scripts.name]['version'], + 'v2', + ); + expect(provider.hasPendingUpdateForTesting, isFalse); + }); + }); +} + +Map _manifestWithArtifactRefresh(Map manifest) { + final artifacts = + Map.from(manifest['artifacts'] as Map); + final config = + Map.from(artifacts['config'] as Map? ?? {}); + final envVariables = Map.from( + config['envVariables'] as Map? ?? {}); + envVariables['ENABLE_ARTIFACT_REFRESH'] = 'true'; + config['envVariables'] = envVariables; + artifacts['config'] = config; + return {'artifacts': artifacts}; } +Map _manifestWithResourceVersion(String version) => { + 'artifacts': { + 'config': {}, + 'screens': [], + 'theme': '', + 'widgets': {}, + 'scripts': { + 'version': version, + }, + 'actions': [], + 'translations': [], + } + }; + Future> _pumpTranslationApp( WidgetTester tester, { required CdnDefinitionProvider provider, diff --git a/modules/ensemble/test/upload_batch_split_test.dart b/modules/ensemble/test/upload_batch_split_test.dart new file mode 100644 index 000000000..fa9e118bf --- /dev/null +++ b/modules/ensemble/test/upload_batch_split_test.dart @@ -0,0 +1,36 @@ +import 'package:ensemble/util/upload_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('splitUploadFileBatches', () { + test('returns a single batch when batchSize is null', () { + expect(splitUploadFileBatches([1, 2, 3], null), [ + [1, 2, 3], + ]); + }); + + test('splits files into fixed-size batches', () { + expect(splitUploadFileBatches([1, 2, 3, 4, 5], 2), [ + [1, 2], + [3, 4], + [5], + ]); + }); + + test('returns one batch when batchSize covers all files', () { + expect(splitUploadFileBatches(['a', 'b'], 10), [ + ['a', 'b'], + ]); + }); + + test('returns empty outer list when batchSize set but input is empty', () { + expect(splitUploadFileBatches([], 2), isEmpty); + }); + + test('returns one empty batch when batchSize is null and input is empty', () { + expect(splitUploadFileBatches([], null), [ + [], + ]); + }); + }); +}