Nikolas Rimikis
1 year ago
committed by
GitHub
19 changed files with 568 additions and 199 deletions
@ -0,0 +1,40 @@ |
|||||||
|
import 'dart:async'; |
||||||
|
|
||||||
|
import 'package:collection/collection.dart'; |
||||||
|
import 'package:neon/src/settings/models/option.dart'; |
||||||
|
|
||||||
|
/// Exportable data interface. |
||||||
|
abstract interface class Exportable { |
||||||
|
/// Exports into a json map entry. |
||||||
|
MapEntry<String, Object?> export(); |
||||||
|
|
||||||
|
/// Imports [data] from an export. |
||||||
|
FutureOr<void> import(final Map<String, Object?> data); |
||||||
|
} |
||||||
|
|
||||||
|
/// Serialization helpers for a collection of [Option]s. |
||||||
|
extension SerializeOptions on Iterable<Option<dynamic>> { |
||||||
|
/// Serializes into an [Iterable<JsonEntry>]. |
||||||
|
/// |
||||||
|
/// Use [Map.fromEntries] to get a json Map. |
||||||
|
Iterable<MapEntry<String, Object?>> serialize() sync* { |
||||||
|
for (final option in this) { |
||||||
|
if (option.enabled) { |
||||||
|
yield MapEntry(option.key.value, option.serialize()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Deserializes [data] and updates the [Option]s. |
||||||
|
void deserialize(final Map<String, Object?> data) { |
||||||
|
for (final entry in data.entries) { |
||||||
|
final option = firstWhereOrNull((final option) => option.key.value == entry.key); |
||||||
|
|
||||||
|
if (entry.value != null) { |
||||||
|
option?.load(entry.value); |
||||||
|
} else { |
||||||
|
option?.reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,23 +0,0 @@ |
|||||||
import 'package:neon/src/settings/models/option.dart'; |
|
||||||
import 'package:neon/src/settings/models/options_category.dart'; |
|
||||||
import 'package:neon/src/settings/models/storage.dart'; |
|
||||||
|
|
||||||
abstract class NextcloudAppOptions { |
|
||||||
NextcloudAppOptions(this.storage); |
|
||||||
|
|
||||||
final AppStorage storage; |
|
||||||
late final List<OptionsCategory> categories; |
|
||||||
late final List<Option> options; |
|
||||||
|
|
||||||
void reset() { |
|
||||||
for (final option in options) { |
|
||||||
option.reset(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
void dispose() { |
|
||||||
for (final option in options) { |
|
||||||
option.dispose(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,65 @@ |
|||||||
|
import 'package:meta/meta.dart'; |
||||||
|
import 'package:neon/src/settings/models/exportable.dart'; |
||||||
|
import 'package:neon/src/settings/models/option.dart'; |
||||||
|
import 'package:neon/src/settings/models/options_category.dart'; |
||||||
|
import 'package:neon/src/settings/models/storage.dart'; |
||||||
|
|
||||||
|
/// Collection of [Option]s. |
||||||
|
abstract class OptionsCollection implements Exportable { |
||||||
|
OptionsCollection(this.storage); |
||||||
|
|
||||||
|
/// Storage backend to use. |
||||||
|
@protected |
||||||
|
final AppStorage storage; |
||||||
|
|
||||||
|
/// Collection of options. |
||||||
|
@protected |
||||||
|
Iterable<Option> get options; |
||||||
|
|
||||||
|
/// Resets all [options]. |
||||||
|
/// |
||||||
|
/// Implementers extending this must call super. |
||||||
|
@mustCallSuper |
||||||
|
void reset() { |
||||||
|
for (final option in options) { |
||||||
|
option.reset(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Disposes all [options]. |
||||||
|
/// |
||||||
|
/// Implementers extending this must call super. |
||||||
|
@mustCallSuper |
||||||
|
void dispose() { |
||||||
|
for (final option in options) { |
||||||
|
option.dispose(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
MapEntry<String, Object?> export() { |
||||||
|
final data = Map.fromEntries(options.serialize()); |
||||||
|
|
||||||
|
return MapEntry(storage.id, data); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void import(final Map<String, Object?> data) { |
||||||
|
final values = data[storage.id] as Map<String, Object?>?; |
||||||
|
|
||||||
|
if (values != null) { |
||||||
|
options.deserialize(values); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// OpptionsCollection for a neon app. |
||||||
|
abstract class NextcloudAppOptions extends OptionsCollection { |
||||||
|
NextcloudAppOptions(super.storage); |
||||||
|
|
||||||
|
/// Collection of categories to display the options in the settings. |
||||||
|
late final Iterable<OptionsCategory> categories; |
||||||
|
|
||||||
|
@override |
||||||
|
late final Iterable<Option> options; |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
import 'dart:async'; |
||||||
|
import 'dart:convert'; |
||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:meta/meta.dart'; |
||||||
|
import 'package:neon/src/blocs/accounts.dart'; |
||||||
|
import 'package:neon/src/models/account.dart' show Account, AccountFind; |
||||||
|
import 'package:neon/src/models/app_implementation.dart'; |
||||||
|
import 'package:neon/src/settings/models/exportable.dart'; |
||||||
|
import 'package:neon/src/settings/models/option.dart'; |
||||||
|
import 'package:neon/src/settings/models/storage.dart'; |
||||||
|
|
||||||
|
/// Helper class to export all [Option]s. |
||||||
|
/// |
||||||
|
/// Json based operations: |
||||||
|
/// * [exportToJson] |
||||||
|
/// * [applyFromJson] |
||||||
|
/// |
||||||
|
/// [Uint8List] based operations: |
||||||
|
/// * [exportToFile] |
||||||
|
/// * [applyFromFile] |
||||||
|
@internal |
||||||
|
@immutable |
||||||
|
class SettingsExportHelper { |
||||||
|
const SettingsExportHelper({ |
||||||
|
required this.exportables, |
||||||
|
}); |
||||||
|
|
||||||
|
/// Collections of elements to export. |
||||||
|
final Set<Exportable> exportables; |
||||||
|
|
||||||
|
/// Imports [file] and applies the stored [Option]s. |
||||||
|
/// |
||||||
|
/// See: |
||||||
|
/// * [applyFromJson] to import a json map. |
||||||
|
Future<void> applyFromFile(final Stream<List<int>>? file) async { |
||||||
|
final transformer = const Utf8Decoder().fuse(const JsonDecoder()); |
||||||
|
|
||||||
|
final data = await file?.transform(transformer).single; |
||||||
|
|
||||||
|
if (data == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
await applyFromJson(data as Map<String, Object?>); |
||||||
|
} |
||||||
|
|
||||||
|
/// Imports the [json] data and applies the stored [Option]s. |
||||||
|
/// |
||||||
|
/// See: |
||||||
|
/// * [applyFromFile] to import data from a [Stream<Uint8List>]. |
||||||
|
Future<void> applyFromJson(final Map<String, Object?> json) async { |
||||||
|
for (final exportable in exportables) { |
||||||
|
await exportable.import(json); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Exports the stored [Option]s into a [Uint8List]. |
||||||
|
/// |
||||||
|
/// See: |
||||||
|
/// * [exportToJson] to export to a json map. |
||||||
|
Uint8List exportToFile() { |
||||||
|
final transformer = JsonUtf8Encoder(); |
||||||
|
|
||||||
|
final json = exportToJson(); |
||||||
|
return transformer.convert(json) as Uint8List; |
||||||
|
} |
||||||
|
|
||||||
|
/// Exports the stored [Option]s into a json map. |
||||||
|
/// |
||||||
|
/// See: |
||||||
|
/// * [exportToFile] to export data to a [Uint8List]. |
||||||
|
Map<String, Object?> exportToJson() => Map.fromEntries(exportables.map((final e) => e.export())); |
||||||
|
} |
||||||
|
|
||||||
|
/// Helper class to export [AppImplementation]s implementing the [Exportable] interface. |
||||||
|
@immutable |
||||||
|
class AppImplementationsExporter implements Exportable { |
||||||
|
const AppImplementationsExporter(this.appImplementations); |
||||||
|
|
||||||
|
/// List of apps to export. |
||||||
|
final Iterable<AppImplementation> appImplementations; |
||||||
|
|
||||||
|
/// Key the exported value will be stored at. |
||||||
|
static final _key = StorageKeys.apps.value; |
||||||
|
|
||||||
|
@override |
||||||
|
MapEntry<String, Object?> export() => MapEntry( |
||||||
|
_key, |
||||||
|
Map.fromEntries(appImplementations.map((final app) => app.options.export())), |
||||||
|
); |
||||||
|
|
||||||
|
@override |
||||||
|
void import(final Map<String, Object?> data) { |
||||||
|
final values = data[_key] as Map<String, Object?>?; |
||||||
|
|
||||||
|
if (values == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (final element in values.entries) { |
||||||
|
final app = appImplementations.tryFind(element.key); |
||||||
|
|
||||||
|
if (app != null) { |
||||||
|
app.options.import(values); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Helper class to export [Account]s implementing the [Exportable] interface. |
||||||
|
@immutable |
||||||
|
class AccountsBlocExporter implements Exportable { |
||||||
|
const AccountsBlocExporter(this.accountsBloc); |
||||||
|
|
||||||
|
/// AccountsBloc containing the accounts to export. |
||||||
|
final AccountsBloc accountsBloc; |
||||||
|
|
||||||
|
/// Key the exported value will be stored at. |
||||||
|
static final _key = StorageKeys.accounts.value; |
||||||
|
|
||||||
|
@override |
||||||
|
MapEntry<String, Object?> export() => MapEntry(_key, Map.fromEntries(_serialize())); |
||||||
|
|
||||||
|
Iterable<MapEntry<String, Object?>> _serialize() sync* { |
||||||
|
for (final account in accountsBloc.accounts.value) { |
||||||
|
yield accountsBloc.getOptionsFor(account).export(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void import(final Map<String, Object?> data) { |
||||||
|
final values = data[_key] as Map<String, Object?>?; |
||||||
|
|
||||||
|
if (values == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
for (final element in values.entries) { |
||||||
|
final account = accountsBloc.accounts.value.tryFind(element.key); |
||||||
|
|
||||||
|
if (account != null) { |
||||||
|
accountsBloc.getOptionsFor(account).import(values); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,98 +0,0 @@ |
|||||||
import 'package:meta/meta.dart'; |
|
||||||
import 'package:neon/src/models/account.dart'; |
|
||||||
import 'package:neon/src/models/app_implementation.dart'; |
|
||||||
import 'package:neon/src/settings/models/option.dart'; |
|
||||||
import 'package:neon/src/utils/global_options.dart'; |
|
||||||
|
|
||||||
@internal |
|
||||||
class SettingsExportHelper { |
|
||||||
SettingsExportHelper({ |
|
||||||
required this.globalOptions, |
|
||||||
required this.appImplementations, |
|
||||||
required this.accountSpecificOptions, |
|
||||||
}); |
|
||||||
|
|
||||||
final GlobalOptions globalOptions; |
|
||||||
final Iterable<AppImplementation> appImplementations; |
|
||||||
final Map<Account, List<Option>> accountSpecificOptions; |
|
||||||
|
|
||||||
Future applyFromJson(final Map<String, dynamic> data) async { |
|
||||||
final globalOptionsData = data['global'] as Map<String, dynamic>; |
|
||||||
await _applyOptionsMapToOptions( |
|
||||||
globalOptions.options, |
|
||||||
globalOptionsData, |
|
||||||
); |
|
||||||
|
|
||||||
final appImplementationsData = data['apps'] as Map<String, dynamic>; |
|
||||||
for (final appId in appImplementationsData.keys) { |
|
||||||
final app = appImplementations.tryFind(appId); |
|
||||||
if (app == null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
final appImplementationData = appImplementationsData[appId]! as Map<String, dynamic>; |
|
||||||
await _applyOptionsMapToOptions( |
|
||||||
app.options.options, |
|
||||||
appImplementationData, |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
final accountsData = data['accounts'] as Map<String, dynamic>; |
|
||||||
for (final accountId in accountsData.keys) { |
|
||||||
final account = accountSpecificOptions.keys.tryFind(accountId); |
|
||||||
if (account == null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
final accountData = accountsData[accountId]! as Map<String, dynamic>; |
|
||||||
await _applyOptionsMapToOptions( |
|
||||||
accountSpecificOptions[account]!, |
|
||||||
accountData, |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future _applyOptionsMapToOptions(final List<Option> options, final Map<String, dynamic> data) async { |
|
||||||
for (final optionKey in data.keys) { |
|
||||||
for (final option in options) { |
|
||||||
if (option.key.value == optionKey) { |
|
||||||
final Object? value = data[optionKey]; |
|
||||||
|
|
||||||
if (value != null) { |
|
||||||
option.value = await option.deserialize(value); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Map<String, dynamic> toJsonExport() => { |
|
||||||
'global': { |
|
||||||
for (final option in globalOptions.options) ...{ |
|
||||||
if (option.enabled) ...{ |
|
||||||
option.key: option.serialize(), |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
'apps': { |
|
||||||
for (final appImplementation in appImplementations) ...{ |
|
||||||
appImplementation.id: { |
|
||||||
for (final option in appImplementation.options.options) ...{ |
|
||||||
if (option.enabled) ...{ |
|
||||||
option.key: option.serialize(), |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
'accounts': { |
|
||||||
for (final account in accountSpecificOptions.keys) ...{ |
|
||||||
account.id: { |
|
||||||
for (final option in accountSpecificOptions[account]!) ...{ |
|
||||||
if (option.enabled) ...{ |
|
||||||
option.key: option.serialize(), |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}, |
|
||||||
}; |
|
||||||
} |
|
@ -0,0 +1,84 @@ |
|||||||
|
import 'package:mocktail/mocktail.dart'; |
||||||
|
import 'package:neon/settings.dart'; |
||||||
|
import 'package:neon/src/settings/models/option.dart'; |
||||||
|
import 'package:neon/src/settings/models/storage.dart'; |
||||||
|
import 'package:test/test.dart'; |
||||||
|
|
||||||
|
// ignore: missing_override_of_must_be_overridden |
||||||
|
class OptionMock extends Mock implements Option {} |
||||||
|
|
||||||
|
class Collection extends NextcloudAppOptions { |
||||||
|
Collection(final List<Option> options) : super(const AppStorage(StorageKeys.apps)) { |
||||||
|
super.options = options; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
enum Keys implements Storable { |
||||||
|
key1._('key1'), |
||||||
|
key2._('key2'); |
||||||
|
|
||||||
|
const Keys._(this.value); |
||||||
|
|
||||||
|
@override |
||||||
|
final String value; |
||||||
|
} |
||||||
|
|
||||||
|
void main() { |
||||||
|
group('OptionsCollection', () { |
||||||
|
final option1 = OptionMock(); |
||||||
|
final option2 = OptionMock(); |
||||||
|
final collection = Collection([ |
||||||
|
option1, |
||||||
|
option2, |
||||||
|
]); |
||||||
|
|
||||||
|
test('reset', () { |
||||||
|
collection.reset(); |
||||||
|
|
||||||
|
verify(option1.reset).called(1); |
||||||
|
verify(option2.reset).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('dispose', () { |
||||||
|
collection.dispose(); |
||||||
|
|
||||||
|
verify(option1.dispose).called(1); |
||||||
|
verify(option2.dispose).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('export', () { |
||||||
|
when(() => option1.key).thenReturn(Keys.key1); |
||||||
|
when(option1.serialize).thenReturn('value1'); |
||||||
|
when(() => option1.enabled).thenReturn(true); |
||||||
|
|
||||||
|
when(() => option2.key).thenReturn(Keys.key2); |
||||||
|
when(option2.serialize).thenReturn('value2'); |
||||||
|
when(() => option2.enabled).thenReturn(false); |
||||||
|
|
||||||
|
const json = { |
||||||
|
'app': {'key1': 'value1'}, |
||||||
|
}; |
||||||
|
|
||||||
|
final export = collection.export(); |
||||||
|
|
||||||
|
expect(Map.fromEntries([export]), equals(json)); |
||||||
|
}); |
||||||
|
|
||||||
|
test('import', () { |
||||||
|
when(() => option1.key).thenReturn(Keys.key1); |
||||||
|
when(() => option2.key).thenReturn(Keys.key2); |
||||||
|
|
||||||
|
const json = { |
||||||
|
'app': { |
||||||
|
'key1': 'value1', |
||||||
|
'key2': null, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
collection.import(json); |
||||||
|
|
||||||
|
verify(() => option1.load('value1')).called(1); |
||||||
|
verify(option2.reset).called(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
// ignore_for_file: avoid_implementing_value_types |
||||||
|
|
||||||
|
import 'dart:convert'; |
||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:mocktail/mocktail.dart'; |
||||||
|
import 'package:neon/blocs.dart'; |
||||||
|
import 'package:neon/src/models/account.dart'; |
||||||
|
import 'package:neon/src/models/app_implementation.dart'; |
||||||
|
import 'package:neon/src/settings/models/exportable.dart'; |
||||||
|
import 'package:neon/src/settings/models/options_collection.dart'; |
||||||
|
import 'package:neon/src/settings/utils/settings_export_helper.dart'; |
||||||
|
import 'package:neon/src/utils/account_options.dart'; |
||||||
|
import 'package:rxdart/rxdart.dart'; |
||||||
|
import 'package:test/test.dart'; |
||||||
|
|
||||||
|
// ignore: missing_override_of_must_be_overridden |
||||||
|
class FakeAppImplementation extends Mock implements AppImplementation {} |
||||||
|
|
||||||
|
class NextcloudAppOptionsMock extends Mock implements NextcloudAppOptions {} |
||||||
|
|
||||||
|
class AccountsBlocMock extends Mock implements AccountsBloc {} |
||||||
|
|
||||||
|
class FakeAccount extends Mock implements Account {} |
||||||
|
|
||||||
|
class AccountSpecificOptionsMock extends Mock implements AccountSpecificOptions {} |
||||||
|
|
||||||
|
class ExporterMock extends Mock implements Exportable {} |
||||||
|
|
||||||
|
void main() { |
||||||
|
group('Exporter', () { |
||||||
|
test('AccountsBlocExporter', () { |
||||||
|
var exporter = const AppImplementationsExporter([]); |
||||||
|
|
||||||
|
var export = exporter.export(); |
||||||
|
expect(Map.fromEntries([export]), {'app': {}}); |
||||||
|
|
||||||
|
final fakeApp = FakeAppImplementation(); |
||||||
|
final fakeOptions = NextcloudAppOptionsMock(); |
||||||
|
exporter = AppImplementationsExporter([fakeApp]); |
||||||
|
|
||||||
|
const appValue = MapEntry('appID', 'value'); |
||||||
|
const appExport = { |
||||||
|
'app': {'appID': 'value'}, |
||||||
|
}; |
||||||
|
|
||||||
|
when(() => fakeApp.options).thenReturn(fakeOptions); |
||||||
|
when(fakeOptions.export).thenReturn(appValue); |
||||||
|
when(() => fakeApp.id).thenReturn('appID'); |
||||||
|
|
||||||
|
export = exporter.export(); |
||||||
|
expect(Map.fromEntries([export]), appExport); |
||||||
|
|
||||||
|
exporter.import(Map.fromEntries([export])); |
||||||
|
verify(() => fakeOptions.import(Map.fromEntries([appValue]))).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('AccountsBlocExporter', () { |
||||||
|
final bloc = AccountsBlocMock(); |
||||||
|
final exporter = AccountsBlocExporter(bloc); |
||||||
|
|
||||||
|
const accountValue = MapEntry('accountID', 'value'); |
||||||
|
const accountExport = { |
||||||
|
'accounts': {'accountID': 'value'}, |
||||||
|
}; |
||||||
|
|
||||||
|
when(() => bloc.accounts).thenAnswer((final _) => BehaviorSubject.seeded([])); |
||||||
|
var export = exporter.export(); |
||||||
|
expect(Map.fromEntries([export]), {'accounts': {}}); |
||||||
|
|
||||||
|
final fakeAccount = FakeAccount(); |
||||||
|
final fakeOptions = AccountSpecificOptionsMock(); |
||||||
|
when(() => bloc.accounts).thenAnswer((final _) => BehaviorSubject.seeded([fakeAccount])); |
||||||
|
when(() => bloc.getOptionsFor(fakeAccount)).thenReturn(fakeOptions); |
||||||
|
when(fakeOptions.export).thenReturn(accountValue); |
||||||
|
when(() => fakeAccount.id).thenReturn('accountID'); |
||||||
|
|
||||||
|
export = exporter.export(); |
||||||
|
expect(Map.fromEntries([export]), accountExport); |
||||||
|
|
||||||
|
exporter.import(Map.fromEntries([export])); |
||||||
|
verify(() => fakeOptions.import(Map.fromEntries([accountValue]))).called(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
group('SettingsExportHelper', () { |
||||||
|
test('SettingsExportHelper.json', () async { |
||||||
|
final exportable = ExporterMock(); |
||||||
|
final settingsExporter = SettingsExportHelper( |
||||||
|
exportables: { |
||||||
|
exportable, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const value = MapEntry('sxportableKey', 'value'); |
||||||
|
const export = {'sxportableKey': 'value'}; |
||||||
|
|
||||||
|
when(exportable.export).thenAnswer((final _) => value); |
||||||
|
|
||||||
|
expect(settingsExporter.exportToJson(), equals(export)); |
||||||
|
|
||||||
|
await settingsExporter.applyFromJson(export); |
||||||
|
verify(() => exportable.import(Map.fromEntries([value]))).called(1); |
||||||
|
}); |
||||||
|
|
||||||
|
test('SettingsExportHelper.file', () async { |
||||||
|
final exportable = ExporterMock(); |
||||||
|
final settingsExporter = SettingsExportHelper( |
||||||
|
exportables: { |
||||||
|
exportable, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
const value = MapEntry('sxportableKey', 'value'); |
||||||
|
const jsonExport = {'sxportableKey': 'value'}; |
||||||
|
final export = JsonUtf8Encoder().convert(jsonExport) as Uint8List; |
||||||
|
|
||||||
|
when(exportable.export).thenAnswer((final _) => value); |
||||||
|
|
||||||
|
expect(settingsExporter.exportToFile(), equals(export)); |
||||||
|
|
||||||
|
await settingsExporter.applyFromFile(_streamValue(export)); |
||||||
|
verify(() => exportable.import(Map.fromEntries([value]))).called(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
Stream<T> _streamValue<T>(final T value) async* { |
||||||
|
yield value; |
||||||
|
} |
Loading…
Reference in new issue