diff --git a/example/lib/main.dart b/example/lib/main.dart index 9b8f1eb..39c905e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,16 +29,18 @@ class _MyAppState extends State { OptimizelyDecideOption.includeReasons, OptimizelyDecideOption.excludeVariables }; + final customLogger = CustomLogger(); - var flutterSDK = OptimizelyFlutterSdk("X9mZd2WDywaUL9hZXyh9A", - datafilePeriodicDownloadInterval: 10 * 60, - eventOptions: const EventOptions( - batchSize: 1, timeInterval: 60, maxQueueSize: 10000), - defaultLogLevel: OptimizelyLogLevel.debug, - defaultDecideOptions: defaultOptions, - logger: customLogger, - ); + var flutterSDK = OptimizelyFlutterSdk( + "X9mZd2WDywaUL9hZXyh9A", + datafilePeriodicDownloadInterval: 10 * 60, + eventOptions: const EventOptions( + batchSize: 1, timeInterval: 60, maxQueueSize: 10000), + defaultLogLevel: OptimizelyLogLevel.debug, + defaultDecideOptions: defaultOptions, + logger: customLogger, + ); var response = await flutterSDK.initializeClient(); setState(() { @@ -60,7 +62,7 @@ class _MyAppState extends State { "stringValue": "121" }); - // To add decide listener + // Add decide listener var decideListenerId = await flutterSDK.addDecisionNotificationListener((notification) { print("Parsed decision event ...................."); @@ -73,17 +75,13 @@ class _MyAppState extends State { Set options = { OptimizelyDecideOption.ignoreUserProfileService, }; + // Decide call var decideResponse = await userContext.decide('flag1', options); uiResponse += "\nFirst decide call variationKey: ${decideResponse.decision!.variationKey}"; - // should return following response without forced decision - // flagKey: flag1 - // ruleKey: default-rollout-7371-20896892800 - // variationKey: off - - // Setting forced decision + // Set forced decision await userContext.setForcedDecision( OptimizelyDecisionContext("flag1", "flag1_experiment"), OptimizelyForcedDecision("variation_a")); @@ -93,11 +91,6 @@ class _MyAppState extends State { uiResponse += "\nSecond decide call variationKey: ${decideResponse.decision!.variationKey}"; - // should return following response with forced decision - // flagKey: flag1 - // ruleKey: flag1_experiment - // variationKey: variation_a - // removing forced decision await userContext.removeForcedDecision( OptimizelyDecisionContext("flag1", "flag1_experiment")); @@ -111,14 +104,6 @@ class _MyAppState extends State { uiResponse = uiResponse; }); - // should return original response without forced decision - // flagKey: flag1 - // ruleKey: default-rollout-7371-20896892800 - // variationKey: off - - // To cancel decide listener - // await flutterSDK.removeNotificationListener(decideListenerId); - // To add track listener var trackListenerID = await flutterSDK.addTrackNotificationListener((notification) { @@ -137,12 +122,19 @@ class _MyAppState extends State { print("log event notification received"); }); - // Track call + // Track call with nested objects response = await userContext.trackEvent("myevent", { - "age": 20, - "doubleValue": 12.12, - "boolValue": false, - "stringValue": "121" + "revenue": 99.99, + "user": { + "id": "user123", + "premium": true, + "tags": ["vip", "loyal"] + }, + "items": [ + {"name": "Product A", "quantity": 2, "price": 49.99}, + {"name": "Product B", "quantity": 1, "price": 50.00} + ], + "metadata": {"source": "mobile_app", "platform": "ios"} }); // To cancel track listener diff --git a/ios/Classes/HelperClasses/Constants.swift b/ios/Classes/HelperClasses/Constants.swift index a29370a..f80735c 100644 --- a/ios/Classes/HelperClasses/Constants.swift +++ b/ios/Classes/HelperClasses/Constants.swift @@ -139,4 +139,6 @@ struct TypeValue { static let int = "int" static let double = "double" static let bool = "bool" + static let map = "map" + static let list = "list" } diff --git a/ios/Classes/HelperClasses/Utils.swift b/ios/Classes/HelperClasses/Utils.swift index 41b39c1..7636b68 100644 --- a/ios/Classes/HelperClasses/Utils.swift +++ b/ios/Classes/HelperClasses/Utils.swift @@ -26,36 +26,59 @@ public class Utils: NSObject { } var typedDictionary = [String: Any]() for (k,v) in args { - if let typedValue = v as? Dictionary, let value = typedValue["value"] as? Any, let type = typedValue["type"] as? String { - switch type { - case TypeValue.string: - if let strValue = value as? String { - typedDictionary[k] = strValue - } - break - case TypeValue.int: - if let intValue = value as? Int { - typedDictionary[k] = NSNumber(value: intValue).intValue - } - break - case TypeValue.double: - if let doubleValue = value as? Double { - typedDictionary[k] = NSNumber(value: doubleValue).doubleValue - } - break - case TypeValue.bool: - if let booleanValue = value as? Bool { - typedDictionary[k] = NSNumber(value: booleanValue).boolValue - } - break - default: - break - } + if let processedValue = processTypedValue(v) { + typedDictionary[k] = processedValue } - continue } return typedDictionary } + + /// Recursively processes typed values from Flutter to native Swift types + private static func processTypedValue(_ value: Any?) -> Any? { + guard let typedValue = value as? Dictionary, + let val = typedValue["value"], + let type = typedValue["type"] as? String else { + return nil + } + + switch type { + case TypeValue.string: + return val as? String + case TypeValue.int: + if let intValue = val as? Int { + return NSNumber(value: intValue).intValue + } + return nil + case TypeValue.double: + if let doubleValue = val as? Double { + return NSNumber(value: doubleValue).doubleValue + } + return nil + case TypeValue.bool: + if let booleanValue = val as? Bool { + return NSNumber(value: booleanValue).boolValue + } + return nil + case TypeValue.map: + guard let nestedMap = val as? Dictionary else { + return nil + } + var result = [String: Any]() + for (k, v) in nestedMap { + if let processedValue = processTypedValue(v) { + result[k] = processedValue + } + } + return result + case TypeValue.list: + guard let nestedArray = val as? [Any?] else { + return nil + } + return nestedArray.compactMap { processTypedValue($0) } + default: + return nil + } + } /// Returns callback required for LogEventListener static func getLogEventCallback(id: Int, sdkKey: String) -> LogEventListener { diff --git a/lib/src/utils/constants.dart b/lib/src/utils/constants.dart index 2bb5421..f5874c1 100644 --- a/lib/src/utils/constants.dart +++ b/lib/src/utils/constants.dart @@ -20,6 +20,8 @@ class Constants { static const String intType = "int"; static const String doubleType = "double"; static const String boolType = "bool"; + static const String mapType = "map"; + static const String listType = "list"; // Supported Method Names static const String initializeMethod = "initialize"; diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 8b18b13..e697b80 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -33,7 +33,17 @@ class Utils { OptimizelySegmentOption.resetCache: "resetCache", }; - static Map convertToTypedMap(Map map) { + /// Converts a map to platform-specific typed format + /// + /// On iOS, returns a typed map with type information for proper native conversion. + /// On Android, returns the original primitive map. + /// + /// The [forceIOSFormat] parameter is used for testing purposes only to test + /// iOS format conversion without running on actual iOS platform. + static Map convertToTypedMap( + Map map, { + bool forceIOSFormat = false, + }) { if (map.isEmpty) { return map; } @@ -43,48 +53,78 @@ class Utils { // Only keep primitive values Map primitiveMap = {}; for (MapEntry e in map.entries) { - if (e.value is String) { + dynamic processedValue = _processValue(e.value); + if (processedValue != null) { primitiveMap[e.key] = e.value; - typedMap[e.key] = { - Constants.value: e.value, - Constants.type: Constants.stringType - }; - continue; + typedMap[e.key] = processedValue; } - if (e.value is double) { - primitiveMap[e.key] = e.value; - typedMap[e.key] = { - Constants.value: e.value, - Constants.type: Constants.doubleType - }; - continue; - } - if (e.value is int) { - primitiveMap[e.key] = e.value; - typedMap[e.key] = { - Constants.value: e.value, - Constants.type: Constants.intType - }; - continue; - } - if (e.value is bool) { - primitiveMap[e.key] = e.value; - typedMap[e.key] = { - Constants.value: e.value, - Constants.type: Constants.boolType - }; - continue; - } - // ignore: avoid_print - print('Unsupported value type for key: ${e.key}.'); } - if (Platform.isIOS) { + if (Platform.isIOS || forceIOSFormat) { return typedMap; } return primitiveMap; } + /// Recursively processes values to add type information for iOS + static dynamic _processValue(dynamic value) { + if (value is String) { + return { + Constants.value: value, + Constants.type: Constants.stringType + }; + } + if (value is double) { + return { + Constants.value: value, + Constants.type: Constants.doubleType + }; + } + if (value is int) { + return { + Constants.value: value, + Constants.type: Constants.intType + }; + } + if (value is bool) { + return { + Constants.value: value, + Constants.type: Constants.boolType + }; + } + if (value is Map) { + // Handle nested maps + Map nestedMap = {}; + (value as Map).forEach((k, v) { + dynamic processedValue = _processValue(v); + if (processedValue != null) { + nestedMap[k.toString()] = processedValue; + } + }); + return { + Constants.value: nestedMap, + Constants.type: Constants.mapType + }; + } + if (value is List) { + // Handle arrays + List nestedList = []; + for (var item in value) { + dynamic processedValue = _processValue(item); + if (processedValue != null) { + nestedList.add(processedValue); + } + } + return { + Constants.value: nestedList, + Constants.type: Constants.listType + }; + } + // ignore: avoid_print + print('Unsupported value type: ${value.runtimeType}'); + return null; + } + static List convertDecideOptions( Set options) { return options.map((option) => Utils.decideOptions[option]!).toList(); diff --git a/test/nested_object_test.dart b/test/nested_object_test.dart new file mode 100644 index 0000000..5ad903b --- /dev/null +++ b/test/nested_object_test.dart @@ -0,0 +1,487 @@ +/// ************************************************************************** +/// Copyright 2022-2024, Optimizely, Inc. and contributors * +/// * +/// Licensed under the Apache License, Version 2.0 (the "License"); * +/// you may not use this file except in compliance with the License. * +/// You may obtain a copy of the License at * +/// * +/// http://www.apache.org/licenses/LICENSE-2.0 * +/// * +/// Unless required by applicable law or agreed to in writing, software * +/// distributed under the License is distributed on an "AS IS" BASIS, * +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * +/// See the License for the specific language governing permissions and * +/// limitations under the License. * +///**************************************************************************/ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:optimizely_flutter_sdk/src/utils/utils.dart'; +import 'package:optimizely_flutter_sdk/src/utils/constants.dart'; + +void main() { + group('Utils.convertToTypedMap - Android Format (primitiveMap)', () { + // These tests verify Android behavior where original structure is preserved + + test('should preserve nested maps in Android format', () { + final input = { + 'simple': 'value', + 'user': { + 'id': '123', + 'name': 'John', + 'age': 30, + } + }; + + final result = Utils.convertToTypedMap(input); // Default is Android format + + expect(result, isNotNull); + expect(result.containsKey('simple'), true); + expect(result.containsKey('user'), true); + + // Android format: original structure preserved + expect(result['simple'], 'value'); + expect(result['user'], isA()); + final userMap = result['user'] as Map; + expect(userMap['id'], '123'); + expect(userMap['name'], 'John'); + expect(userMap['age'], 30); + }); + + test('should preserve deeply nested maps in Android format', () { + final input = { + 'level1': { + 'level2': { + 'level3': { + 'value': 'deep', + } + } + } + }; + + final result = Utils.convertToTypedMap(input); + + final level1 = result['level1'] as Map; + final level2 = level1['level2'] as Map; + final level3 = level2['level3'] as Map; + expect(level3['value'], 'deep'); + }); + + test('should preserve lists of primitives in Android format', () { + final input = { + 'tags': ['flutter', 'optimizely', 'sdk'], + 'scores': [1, 2, 3, 4, 5], + }; + + final result = Utils.convertToTypedMap(input); + + expect(result['tags'], isA()); + expect((result['tags'] as List).length, 3); + expect((result['tags'] as List)[0], 'flutter'); + + expect(result['scores'], isA()); + expect((result['scores'] as List).length, 5); + expect((result['scores'] as List)[0], 1); + }); + + test('should preserve lists of maps in Android format', () { + final input = { + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, + ] + }; + + final result = Utils.convertToTypedMap(input); + + final users = result['users'] as List; + expect(users.length, 2); + expect((users[0] as Map)['name'], 'Alice'); + expect((users[0] as Map)['age'], 30); + expect((users[1] as Map)['name'], 'Bob'); + expect((users[1] as Map)['age'], 25); + }); + + test('should handle empty collections in Android format', () { + final input = { + 'emptyMap': {}, + 'emptyList': [], + 'name': 'test', + }; + + final result = Utils.convertToTypedMap(input); + + expect(result['emptyMap'], isA()); + expect((result['emptyMap'] as Map).isEmpty, true); + expect(result['emptyList'], isA()); + expect((result['emptyList'] as List).isEmpty, true); + expect(result['name'], 'test'); + }); + }); + + group('Utils.convertToTypedMap - iOS Format (typedMap)', () { + // These tests verify iOS behavior where types are wrapped + + test('should wrap primitive types with type information for iOS', () { + final input = { + 'stringKey': 'value', + 'intKey': 42, + 'doubleKey': 3.14, + 'boolKey': true, + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + // iOS format: values are wrapped with type information + expect(result['stringKey'], isA()); + expect(result['stringKey']['value'], 'value'); + expect(result['stringKey']['type'], Constants.stringType); + + expect(result['intKey']['value'], 42); + expect(result['intKey']['type'], Constants.intType); + + expect(result['doubleKey']['value'], 3.14); + expect(result['doubleKey']['type'], Constants.doubleType); + + expect(result['boolKey']['value'], true); + expect(result['boolKey']['type'], Constants.boolType); + }); + + test('should wrap nested maps with type information for iOS', () { + final input = { + 'user': { + 'id': '123', + 'name': 'John', + 'age': 30, + } + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + // Check outer map type + expect(result['user'], isA()); + expect(result['user']['type'], Constants.mapType); + expect(result['user']['value'], isA()); + + // Check nested values + final nestedMap = result['user']['value'] as Map; + expect(nestedMap['id']['value'], '123'); + expect(nestedMap['id']['type'], Constants.stringType); + expect(nestedMap['name']['value'], 'John'); + expect(nestedMap['name']['type'], Constants.stringType); + expect(nestedMap['age']['value'], 30); + expect(nestedMap['age']['type'], Constants.intType); + }); + + test('should wrap deeply nested maps for iOS', () { + final input = { + 'user': { + 'profile': { + 'preferences': { + 'theme': 'dark', + 'notifications': true, + } + } + } + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + expect(result['user']['type'], Constants.mapType); + final userMap = result['user']['value'] as Map; + + expect(userMap['profile']['type'], Constants.mapType); + final profileMap = userMap['profile']['value'] as Map; + + expect(profileMap['preferences']['type'], Constants.mapType); + final preferencesMap = profileMap['preferences']['value'] as Map; + + expect(preferencesMap['theme']['value'], 'dark'); + expect(preferencesMap['theme']['type'], Constants.stringType); + expect(preferencesMap['notifications']['value'], true); + expect(preferencesMap['notifications']['type'], Constants.boolType); + }); + + test('should wrap lists of primitives for iOS', () { + final input = { + 'tags': ['flutter', 'optimizely', 'sdk'], + 'scores': [1, 2, 3, 4, 5], + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + // Check list is wrapped + expect(result['tags']['type'], Constants.listType); + final tagsList = result['tags']['value'] as List; + expect(tagsList.length, 3); + expect(tagsList[0]['value'], 'flutter'); + expect(tagsList[0]['type'], Constants.stringType); + + expect(result['scores']['type'], Constants.listType); + final scoresList = result['scores']['value'] as List; + expect(scoresList.length, 5); + expect(scoresList[0]['value'], 1); + expect(scoresList[0]['type'], Constants.intType); + }); + + test('should wrap lists of maps for iOS', () { + final input = { + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, + ] + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + expect(result['users']['type'], Constants.listType); + final usersList = result['users']['value'] as List; + expect(usersList.length, 2); + + // Check first user + expect(usersList[0]['type'], Constants.mapType); + final firstUser = usersList[0]['value'] as Map; + expect(firstUser['name']['value'], 'Alice'); + expect(firstUser['name']['type'], Constants.stringType); + expect(firstUser['age']['value'], 30); + expect(firstUser['age']['type'], Constants.intType); + + // Check second user + expect(usersList[1]['type'], Constants.mapType); + final secondUser = usersList[1]['value'] as Map; + expect(secondUser['name']['value'], 'Bob'); + expect(secondUser['age']['value'], 25); + }); + + test('should handle empty collections in iOS format', () { + final input = { + 'emptyMap': {}, + 'emptyList': [], + 'name': 'test', + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + expect(result['emptyMap']['type'], Constants.mapType); + final emptyMapValue = result['emptyMap']['value'] as Map; + expect(emptyMapValue.isEmpty, true); + + expect(result['emptyList']['type'], Constants.listType); + final emptyListValue = result['emptyList']['value'] as List; + expect(emptyListValue.isEmpty, true); + + expect(result['name']['value'], 'test'); + expect(result['name']['type'], Constants.stringType); + }); + + test('should handle mixed complex structures for iOS', () { + final input = { + 'event': 'purchase', + 'revenue': 99.99, + 'user': { + 'id': 'user123', + 'premium': true, + 'tags': ['vip', 'loyal'], + }, + 'items': [ + {'name': 'Product A', 'quantity': 2}, + {'name': 'Product B', 'quantity': 1}, + ], + }; + + final result = Utils.convertToTypedMap(input, forceIOSFormat: true); + + // Check primitives + expect(result['event']['value'], 'purchase'); + expect(result['event']['type'], Constants.stringType); + expect(result['revenue']['value'], 99.99); + expect(result['revenue']['type'], Constants.doubleType); + + // Check nested map + expect(result['user']['type'], Constants.mapType); + final userMap = result['user']['value'] as Map; + expect(userMap['id']['value'], 'user123'); + expect(userMap['premium']['value'], true); + + // Check nested list in map + expect(userMap['tags']['type'], Constants.listType); + final tagsList = userMap['tags']['value'] as List; + expect(tagsList[0]['value'], 'vip'); + + // Check list of maps + expect(result['items']['type'], Constants.listType); + final itemsList = result['items']['value'] as List; + expect(itemsList.length, 2); + expect(itemsList[0]['type'], Constants.mapType); + final item0 = itemsList[0]['value'] as Map; + expect(item0['name']['value'], 'Product A'); + expect(item0['quantity']['value'], 2); + }); + }); + + group('Utils.convertToTypedMap - Real World Scenarios', () { + test('should handle real-world trackEvent example in both formats', () { + final input = { + 'event_type': 'checkout', + 'revenue': 199.99, + 'user': { + 'id': 'user_12345', + 'email': 'user@example.com', + 'is_premium': true, + 'account_age_days': 365, + }, + 'cart': { + 'items': [ + { + 'product_id': 'prod_1', + 'name': 'Widget', + 'price': 99.99, + 'quantity': 1, + }, + { + 'product_id': 'prod_2', + 'name': 'Gadget', + 'price': 100.00, + 'quantity': 1, + }, + ], + 'total_items': 2, + }, + 'metadata': { + 'source': 'mobile_app', + 'platform': 'ios', + 'version': '2.1.0', + }, + }; + + // Test Android format + final androidResult = Utils.convertToTypedMap(input); + expect(androidResult, isNotNull); + expect(androidResult.containsKey('event_type'), true); + expect(androidResult['event_type'], 'checkout'); + + final androidUserMap = androidResult['user'] as Map; + expect(androidUserMap['id'], 'user_12345'); + expect(androidUserMap['is_premium'], true); + + final androidCartMap = androidResult['cart'] as Map; + final androidItems = androidCartMap['items'] as List; + expect(androidItems.length, 2); + expect((androidItems[0] as Map)['product_id'], 'prod_1'); + + // Test iOS format + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + expect(iosResult, isNotNull); + expect(iosResult.containsKey('event_type'), true); + expect(iosResult['event_type']['value'], 'checkout'); + expect(iosResult['event_type']['type'], Constants.stringType); + + final iosUserMap = iosResult['user']['value'] as Map; + expect(iosUserMap['id']['value'], 'user_12345'); + expect(iosUserMap['is_premium']['value'], true); + + final iosCartMap = iosResult['cart']['value'] as Map; + final iosItems = iosCartMap['items']['value'] as List; + expect(iosItems.length, 2); + final iosFirstItem = iosItems[0]['value'] as Map; + expect(iosFirstItem['product_id']['value'], 'prod_1'); + }); + + test('should not throw error on nested objects (regression test)', () { + // This ensures we no longer silently drop nested objects like before + final input = { + 'supported': 'value', + 'nested': { + 'should': 'work', + 'now': true, + } + }; + + // Should work in both formats without error + expect(() => Utils.convertToTypedMap(input), returnsNormally); + expect(() => Utils.convertToTypedMap(input, forceIOSFormat: true), returnsNormally); + + final androidResult = Utils.convertToTypedMap(input); + expect(androidResult.containsKey('nested'), true); + expect((androidResult['nested'] as Map)['should'], 'work'); + + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + expect(iosResult.containsKey('nested'), true); + expect(iosResult['nested']['type'], Constants.mapType); + }); + + test('should handle list with mixed types in both formats', () { + final input = { + 'mixed': [1, 'two', 3.0, true], + }; + + // Android format + final androidResult = Utils.convertToTypedMap(input); + final androidMixed = androidResult['mixed'] as List; + expect(androidMixed[0], 1); + expect(androidMixed[1], 'two'); + expect(androidMixed[2], 3.0); + expect(androidMixed[3], true); + + // iOS format + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + final iosMixed = iosResult['mixed']['value'] as List; + expect(iosMixed[0]['value'], 1); + expect(iosMixed[0]['type'], Constants.intType); + expect(iosMixed[1]['value'], 'two'); + expect(iosMixed[1]['type'], Constants.stringType); + expect(iosMixed[2]['value'], 3.0); + expect(iosMixed[2]['type'], Constants.doubleType); + expect(iosMixed[3]['value'], true); + expect(iosMixed[3]['type'], Constants.boolType); + }); + }); + + group('Utils.convertToTypedMap - Edge Cases', () { + test('should handle empty map', () { + final input = {}; + + final androidResult = Utils.convertToTypedMap(input); + expect(androidResult.isEmpty, true); + + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + expect(iosResult.isEmpty, true); + }); + + test('should handle map with only primitives', () { + final input = { + 'a': 1, + 'b': 'text', + 'c': true, + 'd': 3.14, + }; + + final androidResult = Utils.convertToTypedMap(input); + expect(androidResult['a'], 1); + expect(androidResult['b'], 'text'); + + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + expect(iosResult['a']['value'], 1); + expect(iosResult['b']['value'], 'text'); + }); + + test('should handle deeply nested arrays', () { + final input = { + 'nested': [ + [1, 2, 3], + [4, 5, 6], + ] + }; + + // Android + final androidResult = Utils.convertToTypedMap(input); + expect(((androidResult['nested'] as List)[0] as List)[0], 1); + + // iOS + final iosResult = Utils.convertToTypedMap(input, forceIOSFormat: true); + final iosOuter = iosResult['nested']['value'] as List; + final iosInner = iosOuter[0]['value'] as List; + expect(iosInner[0]['value'], 1); + expect(iosInner[0]['type'], Constants.intType); + }); + }); +}