Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions modules/ensemble/lib/action/upload_files_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,18 +97,8 @@ Future<void> uploadFiles({
: scopeManager?.dataContext.getContextById(action.id!)
as UploadFilesResponse;

List<List<File>> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,20 @@ class CdnDefinitionProvider extends DefinitionProvider {
await _refreshTranslationsAtRuntime();
}

@visibleForTesting
Future<void> handlePendingUpdateForTesting() => _handlePendingUpdate();

@visibleForTesting
bool get hasPendingUpdateForTesting => _hasPendingUpdate;

@visibleForTesting
set hasPendingUpdateForTesting(bool value) => _hasPendingUpdate = value;

@visibleForTesting
void rebuildManifestCacheForTesting(Map<String, dynamic> root) {
_rebuildFromRoot(root);
}

@visibleForTesting
Future<void> loadCachedStateForTesting() => _loadCachedState();

Expand Down
15 changes: 15 additions & 0 deletions modules/ensemble/lib/util/upload_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<T>> splitUploadFileBatches<T>(List<T> files, int? batchSize) {
if (batchSize == null) {
return [files];
}
final batches = <List<T>>[];
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);
}
Expand Down
95 changes: 95 additions & 0 deletions modules/ensemble/test/cdn_provider_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
Expand All @@ -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<String, dynamic> _manifestWithArtifactRefresh(Map<String, dynamic> manifest) {
final artifacts =
Map<String, dynamic>.from(manifest['artifacts'] as Map<String, dynamic>);
final config =
Map<String, dynamic>.from(artifacts['config'] as Map? ?? <String, dynamic>{});
final envVariables = Map<String, dynamic>.from(
config['envVariables'] as Map? ?? <String, dynamic>{});
envVariables['ENABLE_ARTIFACT_REFRESH'] = 'true';
config['envVariables'] = envVariables;
artifacts['config'] = config;
return {'artifacts': artifacts};
}

Map<String, dynamic> _manifestWithResourceVersion(String version) => {
'artifacts': {
'config': <String, dynamic>{},
'screens': <dynamic>[],
'theme': '',
'widgets': <String, dynamic>{},
'scripts': <String, dynamic>{
'version': version,
},
'actions': <dynamic>[],
'translations': <dynamic>[],
}
};

Future<ValueNotifier<int>> _pumpTranslationApp(
WidgetTester tester, {
required CdnDefinitionProvider provider,
Expand Down
36 changes: 36 additions & 0 deletions modules/ensemble/test/upload_batch_split_test.dart
Original file line number Diff line number Diff line change
@@ -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<int>([], 2), isEmpty);
});

test('returns one empty batch when batchSize is null and input is empty', () {
expect(splitUploadFileBatches<int>([], null), [
<int>[],
]);
});
});
}
Loading