From cc384d91a77e04d3c0de5fa30b8f71690d16a852 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 29 Dec 2025 17:22:28 -0300 Subject: [PATCH] Add client attribute, flush and destroy methods --- splitio_web/lib/splitio_web.dart | 116 ++++++++++++++++ splitio_web/lib/src/js_interop.dart | 8 ++ splitio_web/test/splitio_web_test.dart | 124 +++++++++++++++++- .../test/utils/js_interop_test_utils.dart | 45 +++++++ 4 files changed, 286 insertions(+), 7 deletions(-) diff --git a/splitio_web/lib/splitio_web.dart b/splitio_web/lib/splitio_web.dart index dda1bea..0619ba7 100644 --- a/splitio_web/lib/splitio_web.dart +++ b/splitio_web/lib/splitio_web.dart @@ -596,4 +596,120 @@ class SplitioWeb extends SplitioPlatform { return result.toDart; } + + @override + Future> getAllAttributes( + {required String matchingKey, required String? bucketingKey}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.getAttributes.callAsFunction(null) as JSObject; + + return jsObjectToMap(result); + } + + @override + Future setAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName, + required dynamic value}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.setAttribute.callAsFunction( + null, attributeName.toJS, _convertValue(value, true)) as JSBoolean; + + return result.toDart; + } + + @override + Future setAttributes( + {required String matchingKey, + required String? bucketingKey, + required Map attributes}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.setAttributes + .callAsFunction(null, _convertMap(attributes, true)) as JSBoolean; + + return result.toDart; + } + + @override + Future getAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.getAttribute.callAsFunction(null, attributeName.toJS); + + return jsAnyToDart(result); + } + + @override + Future removeAttribute( + {required String matchingKey, + required String? bucketingKey, + required String attributeName}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.removeAttribute + .callAsFunction(null, attributeName.toJS) as JSBoolean; + + return result.toDart; + } + + @override + Future clearAttributes( + {required String matchingKey, required String? bucketingKey}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.clearAttributes.callAsFunction(null) as JSBoolean; + + return result.toDart; + } + + @override + Future flush( + {required String matchingKey, required String? bucketingKey}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.flush.callAsFunction(null) as JSPromise; + + return result.toDart; + } + + @override + Future destroy( + {required String matchingKey, required String? bucketingKey}) async { + final client = await _getClient( + matchingKey: matchingKey, + bucketingKey: bucketingKey, + ); + + final result = client.destroy.callAsFunction(null) as JSPromise; + + return result.toDart; + } } diff --git a/splitio_web/lib/src/js_interop.dart b/splitio_web/lib/src/js_interop.dart index a8838b4..a0e3426 100644 --- a/splitio_web/lib/src/js_interop.dart +++ b/splitio_web/lib/src/js_interop.dart @@ -24,6 +24,14 @@ extension type JS_IBrowserClient._(JSObject _) implements JSObject { external JSFunction getTreatmentsWithConfigByFlagSet; external JSFunction getTreatmentsWithConfigByFlagSets; external JSFunction track; + external JSFunction setAttribute; + external JSFunction getAttribute; + external JSFunction removeAttribute; + external JSFunction setAttributes; + external JSFunction getAttributes; + external JSFunction clearAttributes; + external JSFunction flush; + external JSFunction destroy; } @JS() diff --git a/splitio_web/test/splitio_web_test.dart b/splitio_web/test/splitio_web_test.dart index df48f8a..77453e4 100644 --- a/splitio_web/test/splitio_web_test.dart +++ b/splitio_web/test/splitio_web_test.dart @@ -18,7 +18,6 @@ extension on web.Window { } void main() { - SplitioWeb _platform = SplitioWeb(); final mock = SplitioMock(); @@ -40,7 +39,8 @@ void main() { expect(result, 'on'); expect(mock.calls.last.methodName, 'getTreatment'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['split', {}, {}]); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['split', {}, {}]); }); test('getTreatment with attributes', () async { @@ -196,7 +196,8 @@ void main() { expect(result.toString(), SplitResult('on', 'some-config').toString()); expect(mock.calls.last.methodName, 'getTreatmentWithConfig'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['split1', {}, {}]); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['split1', {}, {}]); }); test('getTreatmentsWithConfig without attributes', () async { @@ -250,7 +251,8 @@ void main() { expect(result, {'split1': 'on', 'split2': 'on'}); expect(mock.calls.last.methodName, 'getTreatmentsByFlagSet'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['set_1', {}, {}]); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['set_1', {}, {}]); }); test('getTreatmentsByFlagSet with attributes', () async { @@ -314,7 +316,8 @@ void main() { SplitResult('on', 'some-config').toString(); })); expect(mock.calls.last.methodName, 'getTreatmentsWithConfigByFlagSet'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['set_1', {}, {}]); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['set_1', {}, {}]); }); test('getTreatmentsWithConfigByFlagSet with attributes', () async { @@ -451,6 +454,111 @@ void main() { }); }); + group('other client methods: attributes, destroy, flush', () { + test('get single attribute', () async { + final result = await _platform.getAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attribute-name'); + + expect(result, 'attr-value'); + expect(mock.calls.last.methodName, 'getAttribute'); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['attribute-name']); + }); + + test('get all attributes', () async { + final result = await _platform.getAllAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect( + result, + equals({ + 'attrBool': true, + 'attrString': 'value', + 'attrInt': 1, + 'attrDouble': 1.1, + 'attrList': ['value1', 100, false], + })); + expect(mock.calls.last.methodName, 'getAttributes'); + expect(mock.calls.last.methodArguments, []); + }); + + test('set attribute', () async { + final result = await _platform.setAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'my_attr', + value: 'attr_value'); + + expect(result, true); + expect(mock.calls.last.methodName, 'setAttribute'); + expect(mock.calls.last.methodArguments.map(jsAnyToDart), + ['my_attr', 'attr_value']); + }); + + test('set multiple attributes', () async { + final result = await _platform.setAttributes( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributes: { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', true], + 'attrNull': null, // not valid. ignored + 'attrMap': {'value5': true} // not valid. ignored + }); + + expect(result, true); + expect(mock.calls.last.methodName, 'setAttributes'); + expect(mock.calls.last.methodArguments.map(jsAnyToDart), [ + { + 'bool_attr': true, + 'number_attr': 25.56, + 'string_attr': 'attr-value', + 'list_attr': ['one', true], + } + ]); + }); + + test('remove attribute', () async { + final result = await _platform.removeAttribute( + matchingKey: 'matching-key', + bucketingKey: 'bucketing-key', + attributeName: 'attr-name'); + + expect(result, true); + expect(mock.calls.last.methodName, 'removeAttribute'); + expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['attr-name']); + }); + + test('clear attributes', () async { + final result = await _platform.clearAttributes( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(result, true); + expect(mock.calls.last.methodName, 'clearAttributes'); + expect(mock.calls.last.methodArguments, []); + }); + + test('flush', () async { + await _platform.flush( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(mock.calls.last.methodName, 'flush'); + expect(mock.calls.last.methodArguments, []); + }); + + test('destroy', () async { + await _platform.destroy( + matchingKey: 'matching-key', bucketingKey: 'bucketing-key'); + + expect(mock.calls.last.methodName, 'destroy'); + expect(mock.calls.last.methodArguments, []); + }); + }); + group('initialization', () { test('init with matching key only', () async { SplitioWeb _platform = SplitioWeb(); @@ -687,7 +795,8 @@ void main() { matchingKey: 'matching-key', bucketingKey: null); expect(mock.calls.last.methodName, 'client'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['matching-key']); + expect( + mock.calls.last.methodArguments.map(jsAnyToDart), ['matching-key']); }); test('get client with new matching key', () async { @@ -695,7 +804,8 @@ void main() { matchingKey: 'new-matching-key', bucketingKey: null); expect(mock.calls.last.methodName, 'client'); - expect(mock.calls.last.methodArguments.map(jsAnyToDart), ['new-matching-key']); + expect(mock.calls.last.methodArguments.map(jsAnyToDart), + ['new-matching-key']); }); test('get client with new matching key and bucketing key', () async { diff --git a/splitio_web/test/utils/js_interop_test_utils.dart b/splitio_web/test/utils/js_interop_test_utils.dart index cd90c15..cfb1fcd 100644 --- a/splitio_web/test/utils/js_interop_test_utils.dart +++ b/splitio_web/test/utils/js_interop_test_utils.dart @@ -1,6 +1,9 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +@JS('Promise.resolve') +external JSPromise _promiseResolve(); + class SplitioMock { final List<({String methodName, List methodArguments})> calls = []; final JSObject splitio = JSObject(); @@ -123,6 +126,48 @@ class SplitioMock { )); return trafficType != null ? true.toJS : false.toJS; }.toJS; + mockClient['setAttribute'] = (JSAny? attributeName, JSAny? attributeValue) { + calls.add(( + methodName: 'setAttribute', + methodArguments: [attributeName, attributeValue] + )); + return true.toJS; + }.toJS; + mockClient['getAttribute'] = (JSAny? attributeName) { + calls.add((methodName: 'getAttribute', methodArguments: [attributeName])); + return 'attr-value'.toJS; + }.toJS; + mockClient['removeAttribute'] = (JSAny? attributeName) { + calls.add( + (methodName: 'removeAttribute', methodArguments: [attributeName])); + return true.toJS; + }.toJS; + mockClient['setAttributes'] = (JSAny? attributes) { + calls.add((methodName: 'setAttributes', methodArguments: [attributes])); + return true.toJS; + }.toJS; + mockClient['getAttributes'] = () { + calls.add((methodName: 'getAttributes', methodArguments: [])); + return { + 'attrBool': true, + 'attrString': 'value', + 'attrInt': 1, + 'attrDouble': 1.1, + 'attrList': ['value1', 100, false], + }.jsify(); + }.toJS; + mockClient['clearAttributes'] = () { + calls.add((methodName: 'clearAttributes', methodArguments: [])); + return true.toJS; + }.toJS; + mockClient['flush'] = () { + calls.add((methodName: 'flush', methodArguments: [])); + return _promiseResolve(); + }.toJS; + mockClient['destroy'] = () { + calls.add((methodName: 'destroy', methodArguments: [])); + return _promiseResolve(); + }.toJS; final mockLog = JSObject(); mockLog['warn'] = (JSAny? arg1) {