From e6f9b13ea4b117ca43087a249e4fa426dcfb85e0 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 19 May 2025 10:26:45 +0300 Subject: [PATCH 1/5] fix readme and freez deps --- cherrypick/README.md | 2 +- examples/client_app/pubspec.lock | 18 ++++++++---------- examples/postly/pubspec.lock | 9 ++++----- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/cherrypick/README.md b/cherrypick/README.md index a4499cb..160ab6b 100644 --- a/cherrypick/README.md +++ b/cherrypick/README.md @@ -30,7 +30,7 @@ Binding().toProvide(() => "hello world"); // Asynchronous lazy initialization Binding().toProvideAsync(() async => "hello async world"); -/ Asynchronous lazy initialization with dynamic parameters +// Asynchronous lazy initialization with dynamic parameters Binding().toProvideAsyncWithParams((params) async => "hello $params"); // Initialization with dynamic parameters diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index 02b6015..43c8489 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -28,19 +28,17 @@ packages: cherrypick: dependency: "direct main" description: - name: cherrypick - sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe - url: "https://pub.dev" - source: hosted - version: "2.0.2" + path: "../../cherrypick" + relative: true + source: path + version: "2.1.0-dev.1" cherrypick_flutter: dependency: "direct main" description: - name: cherrypick_flutter - sha256: ad63ae816b7d1147ffb0a82bcae5a1ea3d51e9d398a79459c619464391a43a79 - url: "https://pub.dev" - source: hosted - version: "1.1.1" + path: "../../cherrypick_flutter" + relative: true + source: path + version: "1.1.1-dev.1" clock: dependency: transitive description: diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index 3fcb01b..7e9843a 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -153,11 +153,10 @@ packages: cherrypick: dependency: "direct main" description: - name: cherrypick - sha256: e1e2b4f3a70cbe7760e479e6ddb7dce2fc85a1bbb2fba6c398efe235ed111dfe - url: "https://pub.dev" - source: hosted - version: "2.0.2" + path: "../../cherrypick" + relative: true + source: path + version: "2.1.0-dev.1" clock: dependency: transitive description: From 53dd4a1005de71bb32722c602d480d7a9a029563 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 19 May 2025 10:36:26 +0300 Subject: [PATCH 2/5] add provide typedef --- cherrypick/lib/src/binding.dart | 9 ++++++--- examples/client_app/pubspec.lock | 4 ++-- examples/postly/pubspec.lock | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index be00592..78c6774 100644 --- a/cherrypick/lib/src/binding.dart +++ b/cherrypick/lib/src/binding.dart @@ -13,6 +13,8 @@ enum Mode { simple, instance, providerInstance, providerInstanceWithParams } +typedef Provider = T? Function(); + typedef ProviderWithParams = T Function(dynamic params); typedef AsyncProvider = Future Function(); @@ -27,11 +29,12 @@ class Binding { late Type _key; late String _name; T? _instance; - T? Function()? _provider; + Provider? _provider; + ProviderWithParams? _providerWithParams; + AsyncProvider? asyncProvider; AsyncProviderWithParams? asyncProviderWithParams; - ProviderWithParams? _providerWithParams; late bool _isSingleton = false; late bool _isNamed = false; @@ -95,7 +98,7 @@ class Binding { /// ENG: Initialization instance via provider [value]. /// /// return [Binding] - Binding toProvide(T Function() value) { + Binding toProvide(Provider value) { _mode = Mode.providerInstance; _provider = value; return this; diff --git a/examples/client_app/pubspec.lock b/examples/client_app/pubspec.lock index 43c8489..1b8f1a3 100644 --- a/examples/client_app/pubspec.lock +++ b/examples/client_app/pubspec.lock @@ -31,14 +31,14 @@ packages: path: "../../cherrypick" relative: true source: path - version: "2.1.0-dev.1" + version: "2.1.0" cherrypick_flutter: dependency: "direct main" description: path: "../../cherrypick_flutter" relative: true source: path - version: "1.1.1-dev.1" + version: "1.1.1" clock: dependency: transitive description: diff --git a/examples/postly/pubspec.lock b/examples/postly/pubspec.lock index 7e9843a..880c933 100644 --- a/examples/postly/pubspec.lock +++ b/examples/postly/pubspec.lock @@ -156,7 +156,7 @@ packages: path: "../../cherrypick" relative: true source: path - version: "2.1.0-dev.1" + version: "2.1.0" clock: dependency: transitive description: From 869f9123bc38735b85c8311defe2892b95b2cde2 Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 19 May 2025 10:55:50 +0300 Subject: [PATCH 3/5] feat: implement toInstanceAync binding --- cherrypick/lib/src/binding.dart | 18 ++++++ cherrypick/lib/src/scope.dart | 7 +++ cherrypick/test/src/binding_test.dart | 79 +++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/cherrypick/lib/src/binding.dart b/cherrypick/lib/src/binding.dart index 78c6774..fe248c5 100644 --- a/cherrypick/lib/src/binding.dart +++ b/cherrypick/lib/src/binding.dart @@ -29,6 +29,7 @@ class Binding { late Type _key; late String _name; T? _instance; + Future? _instanceAsync; Provider? _provider; ProviderWithParams? _providerWithParams; @@ -94,6 +95,17 @@ class Binding { return this; } + /// RU: Инициализация экземляпяра [value]. + /// ENG: Initialization instance [value]. + /// + /// return [Binding] + Binding toInstanceAsync(Future value) { + _mode = Mode.instance; + _instanceAsync = value; + _isSingleton = true; + return this; + } + /// RU: Инициализация экземляпяра  через провайдер [value]. /// ENG: Initialization instance via provider [value]. /// @@ -149,6 +161,12 @@ class Binding { /// return [T] T? get instance => _instance; + /// RU: Поиск экземпляра. + /// ENG: Resolve instance. + /// + /// return [T] + Future? get instanceAsync => _instanceAsync; + /// RU: Поиск экземпляра. /// ENG: Resolve instance. /// diff --git a/cherrypick/lib/src/scope.dart b/cherrypick/lib/src/scope.dart index f0ac55d..cfc9ee1 100644 --- a/cherrypick/lib/src/scope.dart +++ b/cherrypick/lib/src/scope.dart @@ -160,11 +160,18 @@ class Scope { if (binding.key == T && ((!binding.isNamed && named == null) || (binding.isNamed && named == binding.name))) { + if (binding.instanceAsync != null) { + return await binding.instanceAsync; + } + if (binding.asyncProvider != null) { return await binding.asyncProvider?.call(); } if (binding.asyncProviderWithParams != null) { + if (params == null) { + throw StateError('Param is null. Maybe you forget pass it'); + } return await binding.asyncProviderWithParams!(params); } } diff --git a/cherrypick/test/src/binding_test.dart b/cherrypick/test/src/binding_test.dart index 9f69647..134257c 100644 --- a/cherrypick/test/src/binding_test.dart +++ b/cherrypick/test/src/binding_test.dart @@ -312,4 +312,83 @@ void main() { }); }); }); + + test('Binding returns null providerWithParams if not set', () { + final binding = Binding(); + expect(binding.providerWithParams(123), null); + }); + + test('Binding withName changes isNamed to true', () { + final binding = Binding().withName('foo'); + expect(binding.isNamed, true); + expect(binding.name, 'foo'); + }); + + // Проверка singleton provider вызывается один раз + test('Singleton provider only called once', () { + int counter = 0; + final binding = Binding().toProvide(() { + counter++; + return counter; + }).singleton(); + + final first = binding.provider; + final second = binding.provider; + expect(first, equals(second)); + expect(counter, 1); + }); + + // Повторный вызов toInstance влияет на значение + test('Multiple toInstance calls changes instance', () { + final binding = Binding().toInstance(1).toInstance(2); + expect(binding.instance, 2); + }); + + // Проверка mode после chaining + test('Chained withName and singleton preserves mode', () { + final binding = + Binding().toProvide(() => 3).withName("named").singleton(); + expect(binding.mode, Mode.providerInstance); + }); + + group('Check toInstanceAsync.', () { + test('Binding resolves instanceAsync with expected value', () async { + final expectedValue = 42; + final binding = + Binding().toInstanceAsync(Future.value(expectedValue)); + final result = await binding.instanceAsync; + expect(result, equals(expectedValue)); + }); + + test('Binding instanceAsync does not affect instance', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.instance, null); + }); + + test('Binding mode is set to instance', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.mode, Mode.instance); + }); + + test('Binding isSingleton is true after toInstanceAsync', () { + final binding = Binding().toInstanceAsync(Future.value(5)); + expect(binding.isSingleton, isTrue); + }); + + test('Binding withName combines with toInstanceAsync', () async { + final binding = Binding() + .withName('asyncValue') + .toInstanceAsync(Future.value(7)); + expect(binding.isNamed, isTrue); + expect(binding.name, 'asyncValue'); + expect(await binding.instanceAsync, 7); + }); + + test('Binding instanceAsync keeps value after multiple awaits', () async { + final binding = Binding().toInstanceAsync(Future.value(123)); + final result1 = await binding.instanceAsync; + final result2 = await binding.instanceAsync; + expect(result1, equals(result2)); + }); + }); } From 50652a14a9c31fb36714cd381dd2fd4d05904a2a Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 19 May 2025 11:10:10 +0300 Subject: [PATCH 4/5] implement scope tests --- cherrypick/test/src/scope_test.dart | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/cherrypick/test/src/scope_test.dart b/cherrypick/test/src/scope_test.dart index 267cac8..ed1d866 100644 --- a/cherrypick/test/src/scope_test.dart +++ b/cherrypick/test/src/scope_test.dart @@ -73,6 +73,120 @@ void main() { expect(() => scope.resolve(), throwsA(isA())); }); }); + + group('Named dependencies', () { + test('Resolve named binding', () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "first"), + TestModule(value: "second", name: "special") + ]); + expect(scope.resolve(named: "special"), "second"); + expect(scope.resolve(), "first"); + }); + + test('Named binding does not clash with unnamed', () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "foo", name: "bar"), + ]); + expect(() => scope.resolve(), throwsA(isA())); + expect(scope.resolve(named: "bar"), "foo"); + }); + + test("tryResolve returns null for missing named", () { + final scope = Scope(null) + ..installModules([ + TestModule(value: "foo"), + ]); + expect(scope.tryResolve(named: "bar"), isNull); + }); + }); + + group('Provider with params', () { + test('Resolve dependency using providerWithParams', () { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideWithParams((param) => (param as int) * 2); + }), + ]); + expect(scope.resolve(params: 3), 6); + expect( + () => scope.resolve(), + throwsA(isA()), + ); + }); + }); + + group('Async resolution', () { + test('Resolve async instance', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toInstanceAsync(Future.value('async value')); + }), + ]); + expect(await scope.resolveAsync(), "async value"); + }); + + test('Resolve async provider', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideAsync(() async => 7); + }), + ]); + expect(await scope.resolveAsync(), 7); + }); + + test('Resolve async provider with param', () async { + final scope = Scope(null) + ..installModules([ + _InlineModule((m, s) { + m.bind().toProvideAsyncWithParams((x) async => (x as int) * 3); + }), + ]); + expect(await scope.resolveAsync(params: 2), 6); + expect( + () => scope.resolveAsync(), + throwsA(isA()), + ); + }); + + test('tryResolveAsync returns null for missing', () async { + final scope = Scope(null); + final result = await scope.tryResolveAsync(); + expect(result, isNull); + }); + }); + + group("Drop modules", () { + test("After dropModules resolves fail", () { + final scope = Scope(null)..installModules([TestModule(value: 5)]); + expect(scope.resolve(), 5); + scope.dropModules(); + expect(() => scope.resolve(), throwsA(isA())); + }); + }); + + group("Subscope closing", () { + test("closeSubScope removes subscope", () { + final scope = Scope(null); + final subScope = scope.openSubScope("child"); + expect(scope.openSubScope("child"), same(subScope)); + scope.closeSubScope("child"); + final newSubScope = scope.openSubScope("child"); + expect(newSubScope, isNot(same(subScope))); // New instance after close + }); + }); + + group("tryResolve returns null if not found", () { + test("Returns null for missing dependency", () { + final scope = Scope(null); + expect(scope.tryResolve(), isNull); + }); + }); } class TestModule extends Module { @@ -89,3 +203,12 @@ class TestModule extends Module { } } } + +/// Вспомогательный модуль для подстановки builder'а через конструктор +class _InlineModule extends Module { + final void Function(Module, Scope) _builder; + _InlineModule(this._builder); + + @override + void builder(Scope s) => _builder(this, s); +} From e91987c635b64842763a910b726b4f961de1d75f Mon Sep 17 00:00:00 2001 From: Sergey Penkovsky Date: Mon, 19 May 2025 11:14:59 +0300 Subject: [PATCH 5/5] update tests --- cherrypick/test/src/binding_test.dart | 533 ++++++++++---------------- cherrypick/test/src/scope_test.dart | 110 +++--- 2 files changed, 257 insertions(+), 386 deletions(-) diff --git a/cherrypick/test/src/binding_test.dart b/cherrypick/test/src/binding_test.dart index 134257c..4d3ce11 100644 --- a/cherrypick/test/src/binding_test.dart +++ b/cherrypick/test/src/binding_test.dart @@ -2,380 +2,91 @@ import 'package:cherrypick/src/binding.dart'; import 'package:test/test.dart'; void main() { - group('Check instance.', () { - group('Without name.', () { - test('Binding resolves null', () { + // --- Instance binding (synchronous) --- + group('Instance Binding (toInstance)', () { + group('Without name', () { + test('Returns null by default', () { final binding = Binding(); expect(binding.instance, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - + test('Sets mode to instance', () { + final binding = Binding().toInstance(5); expect(binding.mode, Mode.instance); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - + test('isSingleton is true', () { + final binding = Binding().toInstance(5); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - - expect(binding.instance, expectedValue); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding().toInstance(expectedValue); - expect(binding.instance, expectedValue); + test('Stores value', () { + final binding = Binding().toInstance(5); + expect(binding.instance, 5); }); }); - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue'); + group('With name', () { + test('Returns null by default', () { + final binding = Binding().withName('n'); expect(binding.instance, null); }); - test('Binding check mode', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('Sets mode to instance', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.mode, Mode.instance); }); - test('Binding check key', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('Sets key', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.key, int); }); - test('Binding check singleton', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - + test('isSingleton is true', () { + final binding = Binding().withName('n').toInstance(5); expect(binding.isSingleton, true); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - - expect(binding.instance, expectedValue); + test('Stores value', () { + final binding = Binding().withName('n').toInstance(5); + expect(binding.instance, 5); }); - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - - expect(binding.name, 'expectedValue'); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = - Binding().withName('expectedValue').toInstance(expectedValue); - expect(binding.instance, expectedValue); - }); - }); - }); - - group('Check provide.', () { - group('Without name.', () { - test('Binding resolves null', () { - final binding = Binding(); - expect(binding.provider, null); - }); - - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - - expect(binding.mode, Mode.providerInstance); - }); - - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - - expect(binding.isSingleton, false); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - - expect(binding.provider, expectedValue); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding().toProvide(() => expectedValue); - expect(binding.provider, expectedValue); + test('Sets name', () { + final binding = Binding().withName('n').toInstance(5); + expect(binding.name, 'n'); }); }); - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue'); - expect(binding.provider, null); - }); - - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.mode, Mode.providerInstance); - }); - - test('Binding check key', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.key, int); - }); - - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.isSingleton, false); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.provider, expectedValue); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - - expect(binding.name, 'expectedValue'); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue); - expect(binding.provider, expectedValue); - }); + test('Multiple toInstance calls change value', () { + final binding = Binding().toInstance(1).toInstance(2); + expect(binding.instance, 2); }); }); - group('Check Async provider.', () { - test('Binding resolves value asynchronously', () async { - final expectedValue = 5; - final binding = Binding().toProvideAsync(() async => expectedValue); - - final result = await binding.asyncProvider?.call(); - expect(result, expectedValue); + // --- Instance binding (asynchronous) --- + group('Async Instance Binding (toInstanceAsync)', () { + test('Resolves instanceAsync with expected value', () async { + final binding = Binding().toInstanceAsync(Future.value(42)); + expect(await binding.instanceAsync, 42); }); - test('Binding resolves value asynchronously with params', () async { - final expectedValue = 5; - final binding = Binding().toProvideAsyncWithParams( - (param) async => expectedValue + (param as int)); - - final result = await binding.asyncProviderWithParams?.call(3); - expect(result, expectedValue + 3); - }); - }); - - group('Check singleton provide.', () { - group('Without name.', () { - test('Binding resolves null', () { - final binding = Binding().singleton(); - expect(binding.provider, null); - }); - - test('Binding check mode', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - - expect(binding.mode, Mode.providerInstance); - }); - - test('Binding check singleton', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - - expect(binding.isSingleton, true); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - - expect(binding.provider, expectedValue); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = - Binding().toProvide(() => expectedValue).singleton(); - expect(binding.provider, expectedValue); - }); - }); - - group('With name.', () { - test('Binding resolves null', () { - final binding = Binding().withName('expectedValue').singleton(); - expect(binding.provider, null); - }); - - test('Binding check mode', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.mode, Mode.providerInstance); - }); - - test('Binding check key', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.key, int); - }); - - test('Binding check singleton', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.isSingleton, true); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.provider, expectedValue); - }); - - test('Binding check value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - - expect(binding.name, 'expectedValue'); - }); - - test('Binding resolves value', () { - final expectedValue = 5; - final binding = Binding() - .withName('expectedValue') - .toProvide(() => expectedValue) - .singleton(); - expect(binding.provider, expectedValue); - }); - }); - }); - - test('Binding returns null providerWithParams if not set', () { - final binding = Binding(); - expect(binding.providerWithParams(123), null); - }); - - test('Binding withName changes isNamed to true', () { - final binding = Binding().withName('foo'); - expect(binding.isNamed, true); - expect(binding.name, 'foo'); - }); - - // Проверка singleton provider вызывается один раз - test('Singleton provider only called once', () { - int counter = 0; - final binding = Binding().toProvide(() { - counter++; - return counter; - }).singleton(); - - final first = binding.provider; - final second = binding.provider; - expect(first, equals(second)); - expect(counter, 1); - }); - - // Повторный вызов toInstance влияет на значение - test('Multiple toInstance calls changes instance', () { - final binding = Binding().toInstance(1).toInstance(2); - expect(binding.instance, 2); - }); - - // Проверка mode после chaining - test('Chained withName and singleton preserves mode', () { - final binding = - Binding().toProvide(() => 3).withName("named").singleton(); - expect(binding.mode, Mode.providerInstance); - }); - - group('Check toInstanceAsync.', () { - test('Binding resolves instanceAsync with expected value', () async { - final expectedValue = 42; - final binding = - Binding().toInstanceAsync(Future.value(expectedValue)); - final result = await binding.instanceAsync; - expect(result, equals(expectedValue)); - }); - - test('Binding instanceAsync does not affect instance', () { + test('Does not affect instance', () { final binding = Binding().toInstanceAsync(Future.value(5)); expect(binding.instance, null); }); - test('Binding mode is set to instance', () { + test('Sets mode to instance', () { final binding = Binding().toInstanceAsync(Future.value(5)); expect(binding.mode, Mode.instance); }); - test('Binding isSingleton is true after toInstanceAsync', () { + test('isSingleton is true after toInstanceAsync', () { final binding = Binding().toInstanceAsync(Future.value(5)); expect(binding.isSingleton, isTrue); }); - test('Binding withName combines with toInstanceAsync', () async { + test('Composes with withName', () async { final binding = Binding() .withName('asyncValue') .toInstanceAsync(Future.value(7)); @@ -384,11 +95,177 @@ void main() { expect(await binding.instanceAsync, 7); }); - test('Binding instanceAsync keeps value after multiple awaits', () async { + test('Keeps value after multiple awaits', () async { final binding = Binding().toInstanceAsync(Future.value(123)); final result1 = await binding.instanceAsync; final result2 = await binding.instanceAsync; expect(result1, equals(result2)); }); }); + + // --- Provider binding (synchronous) --- + group('Provider Binding (toProvide)', () { + group('Without name', () { + test('Returns null by default', () { + final binding = Binding(); + expect(binding.provider, null); + }); + + test('Sets mode to providerInstance', () { + final binding = Binding().toProvide(() => 5); + expect(binding.mode, Mode.providerInstance); + }); + + test('isSingleton is false by default', () { + final binding = Binding().toProvide(() => 5); + expect(binding.isSingleton, false); + }); + + test('Returns provided value', () { + final binding = Binding().toProvide(() => 5); + expect(binding.provider, 5); + }); + }); + + group('With name', () { + test('Returns null by default', () { + final binding = Binding().withName('n'); + expect(binding.provider, null); + }); + + test('Sets mode to providerInstance', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.mode, Mode.providerInstance); + }); + + test('Sets key', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.key, int); + }); + + test('isSingleton is false by default', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.isSingleton, false); + }); + + test('Returns provided value', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.provider, 5); + }); + + test('Sets name', () { + final binding = Binding().withName('n').toProvide(() => 5); + expect(binding.name, 'n'); + }); + }); + }); + + // --- Async provider binding --- + group('Async Provider Binding', () { + test('Resolves asyncProvider value', () async { + final binding = Binding().toProvideAsync(() async => 5); + expect(await binding.asyncProvider?.call(), 5); + }); + + test('Resolves asyncProviderWithParams value', () async { + final binding = Binding() + .toProvideAsyncWithParams((param) async => 5 + (param as int)); + expect(await binding.asyncProviderWithParams?.call(3), 8); + }); + }); + + // --- Singleton provider binding --- + group('Singleton Provider Binding', () { + group('Without name', () { + test('Returns null if no provider set', () { + final binding = Binding().singleton(); + expect(binding.provider, null); + }); + + test('Sets mode to providerInstance', () { + final binding = Binding().toProvide(() => 5).singleton(); + expect(binding.mode, Mode.providerInstance); + }); + + test('isSingleton is true', () { + final binding = Binding().toProvide(() => 5).singleton(); + expect(binding.isSingleton, true); + }); + + test('Returns singleton value', () { + final binding = Binding().toProvide(() => 5).singleton(); + expect(binding.provider, 5); + }); + + test('Returns same value each call and provider only called once', () { + int counter = 0; + final binding = Binding().toProvide(() { + counter++; + return counter; + }).singleton(); + + final first = binding.provider; + final second = binding.provider; + expect(first, equals(second)); + expect(counter, 1); + }); + }); + + group('With name', () { + test('Returns null if no provider set', () { + final binding = Binding().withName('n').singleton(); + expect(binding.provider, null); + }); + + test('Sets mode to providerInstance', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.mode, Mode.providerInstance); + }); + + test('Sets key', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.key, int); + }); + + test('isSingleton is true', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.isSingleton, true); + }); + + test('Returns singleton value', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.provider, 5); + }); + + test('Sets name', () { + final binding = + Binding().withName('n').toProvide(() => 5).singleton(); + expect(binding.name, 'n'); + }); + }); + + test('Chained withName and singleton preserves mode', () { + final binding = + Binding().toProvide(() => 3).withName("named").singleton(); + expect(binding.mode, Mode.providerInstance); + }); + }); + + // --- WithName / Named binding, isNamed, edge-cases --- + group('Named binding & helpers', () { + test('withName sets isNamed true and stores name', () { + final binding = Binding().withName('foo'); + expect(binding.isNamed, true); + expect(binding.name, 'foo'); + }); + + test('providerWithParams returns null if not set', () { + final binding = Binding(); + expect(binding.providerWithParams(123), null); + }); + }); } diff --git a/cherrypick/test/src/scope_test.dart b/cherrypick/test/src/scope_test.dart index ed1d866..738ae74 100644 --- a/cherrypick/test/src/scope_test.dart +++ b/cherrypick/test/src/scope_test.dart @@ -3,46 +3,44 @@ import 'package:cherrypick/src/scope.dart'; import 'package:test/test.dart'; void main() { - group('Without parent scope.', () { - test('Parent scope is null.', () { + // -------------------------------------------------------------------------- + group('Scope & Subscope Management', () { + test('Scope has no parent if constructed with null', () { final scope = Scope(null); expect(scope.parentScope, null); }); - test('Open sub scope.', () { + test('Can open and retrieve the same subScope by key', () { final scope = Scope(null); final subScope = scope.openSubScope('subScope'); expect(scope.openSubScope('subScope'), subScope); }); - test("Container throws state error if the value can't be resolved", () { + test('closeSubScope removes subscope so next openSubScope returns new', () { + final scope = Scope(null); + final subScope = scope.openSubScope("child"); + expect(scope.openSubScope("child"), same(subScope)); + scope.closeSubScope("child"); + final newSubScope = scope.openSubScope("child"); + expect(newSubScope, isNot(same(subScope))); + }); + }); + + // -------------------------------------------------------------------------- + group('Dependency Resolution (standard)', () { + test("Throws StateError if value can't be resolved", () { final scope = Scope(null); expect(() => scope.resolve(), throwsA(isA())); }); - test('Container resolves value after adding a dependency', () { + test('Resolves value after adding a dependency', () { final expectedValue = 'test string'; final scope = Scope(null) .installModules([TestModule(value: expectedValue)]); expect(scope.resolve(), expectedValue); }); - }); - group('With parent scope.', () { - /* - test( - "Container bind() throws state error (if it's parent already has a resolver)", - () { - final parentScope = new Scope(null) - .installModules([TestModule(value: "string one")]); - final scope = new Scope(parentScope); - - expect( - () => scope.installModules([TestModule(value: "string two")]), - throwsA(isA())); - }); -*/ - test('Container resolve() returns a value from parent container.', () { + test('Returns a value from parent scope', () { final expectedValue = 5; final parentScope = Scope(null); final scope = Scope(parentScope); @@ -52,8 +50,7 @@ void main() { expect(scope.resolve(), expectedValue); }); - test('Container resolve() returns a several value from parent container.', - () { + test('Returns several values from parent container', () { final expectedIntValue = 5; final expectedStringValue = 'Hello world'; final parentScope = Scope(null).installModules([ @@ -66,15 +63,22 @@ void main() { expect(scope.resolve(), expectedStringValue); }); - test("Container resolve() throws a state error if parent hasn't value too.", - () { + test("Throws StateError if parent hasn't value too", () { final parentScope = Scope(null); final scope = Scope(parentScope); expect(() => scope.resolve(), throwsA(isA())); }); + + test("After dropModules resolves fail", () { + final scope = Scope(null)..installModules([TestModule(value: 5)]); + expect(scope.resolve(), 5); + scope.dropModules(); + expect(() => scope.resolve(), throwsA(isA())); + }); }); - group('Named dependencies', () { + // -------------------------------------------------------------------------- + group('Named Dependencies', () { test('Resolve named binding', () { final scope = Scope(null) ..installModules([ @@ -103,7 +107,8 @@ void main() { }); }); - group('Provider with params', () { + // -------------------------------------------------------------------------- + group('Provider with parameters', () { test('Resolve dependency using providerWithParams', () { final scope = Scope(null) ..installModules([ @@ -112,14 +117,12 @@ void main() { }), ]); expect(scope.resolve(params: 3), 6); - expect( - () => scope.resolve(), - throwsA(isA()), - ); + expect(() => scope.resolve(), throwsA(isA())); }); }); - group('Async resolution', () { + // -------------------------------------------------------------------------- + group('Async Resolution', () { test('Resolve async instance', () async { final scope = Scope(null) ..installModules([ @@ -148,10 +151,7 @@ void main() { }), ]); expect(await scope.resolveAsync(params: 2), 6); - expect( - () => scope.resolveAsync(), - throwsA(isA()), - ); + expect(() => scope.resolveAsync(), throwsA(isA())); }); test('tryResolveAsync returns null for missing', () async { @@ -161,34 +161,28 @@ void main() { }); }); - group("Drop modules", () { - test("After dropModules resolves fail", () { - final scope = Scope(null)..installModules([TestModule(value: 5)]); - expect(scope.resolve(), 5); - scope.dropModules(); - expect(() => scope.resolve(), throwsA(isA())); - }); - }); - - group("Subscope closing", () { - test("closeSubScope removes subscope", () { - final scope = Scope(null); - final subScope = scope.openSubScope("child"); - expect(scope.openSubScope("child"), same(subScope)); - scope.closeSubScope("child"); - final newSubScope = scope.openSubScope("child"); - expect(newSubScope, isNot(same(subScope))); // New instance after close - }); - }); - - group("tryResolve returns null if not found", () { - test("Returns null for missing dependency", () { + // -------------------------------------------------------------------------- + group('Optional resolution and error handling', () { + test("tryResolve returns null for missing dependency", () { final scope = Scope(null); expect(scope.tryResolve(), isNull); }); + + // Не реализован: + // test("Container bind() throws state error (if it's parent already has a resolver)", () { + // final parentScope = new Scope(null).installModules([TestModule(value: "string one")]); + // final scope = new Scope(parentScope); + + // expect( + // () => scope.installModules([TestModule(value: "string two")]), + // throwsA(isA())); + // }); }); } +// ---------------------------------------------------------------------------- +// Вспомогательные модули + class TestModule extends Module { final T value; final String? name;