From f74bea64c2b8535099470ac325cf4713ef6649ad Mon Sep 17 00:00:00 2001 From: "Hedden, Kyle Matthew" Date: Fri, 17 May 2024 21:24:58 -0400 Subject: [PATCH] Digital PCT265 story PCT-135: Switch to shallow equals with deep compare on parent in order to pinpoint midmatched models. Unit testing setup. --- MVMCoreUI.xcodeproj/project.pbxproj | 158 ++++ .../xcshareddata/xcschemes/MVMCoreUI.xcscheme | 67 ++ .../Atoms/Buttons/ButtonGroupModel.swift | 12 + .../Atomic/Atoms/Buttons/ButtonModel.swift | 2 +- MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift | 14 + .../Atomic/Atoms/Views/ImageViewModel.swift | 16 + .../Label/LabelAttributeActionModel.swift | 4 - .../Views/Label/LabelAttributeModel.swift | 6 + .../Atomic/Atoms/Views/Label/LabelModel.swift | 6 +- .../LockUps/TitleLockupModel.swift | 11 - .../MoleculeSectionHeaderModel.swift | 5 + .../TabsModel.swift | 2 +- .../TwoButtonViewModel.swift | 12 +- .../Items/AccordionListItemModel.swift | 8 + .../Molecules/Items/ListItemModel.swift | 4 +- .../Items/MoleculeCollectionItemModel.swift | 2 +- .../Items/MoleculeListItemModel.swift | 10 - .../Items/MoleculeStackItemModel.swift | 9 +- .../Molecules/Items/TabsListItemModel.swift | 13 +- .../Buttons/NavigationImageButtonModel.swift | 19 + .../MoleculeContainerModel.swift | 3 +- .../EyebrowHeadlineBodyLinkModel.swift | 5 +- .../HeadlineBodyModel.swift | 14 +- .../Atomic/Organisms/Carousel/Carousel.swift | 2 +- .../Organisms/Carousel/CarouselModel.swift | 6 +- MVMCoreUI/Atomic/Organisms/StackModel.swift | 10 +- .../MoleculeComparisonProtocol.swift | 12 +- .../MoleculeModelProtocol.swift | 3 +- .../ParentMoleculeModelProtocol.swift | 57 +- .../TemplateModelProtocol.swift | 2 +- .../Atomic/Templates/BaseTemplateModel.swift | 14 + .../Templates/ListPageTemplateModel.swift | 10 + .../Templates/MoleculeListTemplate.swift | 2 + .../Templates/ThreeLayerModelBase.swift | 8 + .../BaseControllers/ViewController.swift | 13 +- .../JSON/Modelling/UAD_page_model.json | 807 ++++++++++++++++++ .../JSON/Modelling/UAD_page_model_2.json | 807 ++++++++++++++++++ MVMCoreUITests/MVMCoreUITests.swift | 212 +++++ MVMCoreUITests/TestUtils.swift | 19 + 39 files changed, 2261 insertions(+), 125 deletions(-) create mode 100644 MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme create mode 100644 MVMCoreUITests/JSON/Modelling/UAD_page_model.json create mode 100644 MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json create mode 100644 MVMCoreUITests/MVMCoreUITests.swift create mode 100644 MVMCoreUITests/TestUtils.swift diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 36641951..4893b1d1 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -170,6 +170,11 @@ 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */; }; 5822720B2B1FC55F00F75BAE /* AccessibilityHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */; }; 5822720C2B1FC55F00F75BAE /* RotorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */; }; + 583335592BF64E77001D90D7 /* MVMCoreUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */; }; + 5833355A2BF64E77001D90D7 /* MVMCoreUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */; }; + 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335622BF6509C001D90D7 /* UAD_page_model.json */; }; + 583335652BF6A5C3001D90D7 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583335642BF6A5C3001D90D7 /* TestUtils.swift */; }; + 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */ = {isa = PBXBuildFile; fileRef = 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */; }; 5846ABF62B4762A600FA6C76 /* PollingBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */; }; 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A9DD7C2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift */; }; 58E7561D2BE04C320088BB5D /* MoleculeComparisonProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E7561C2BE04C320088BB5D /* MoleculeComparisonProtocol.swift */; }; @@ -610,6 +615,16 @@ FD99130028E21E4900542CC3 /* RuleNotEqualsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5833355B2BF64E77001D90D7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D29DF0C321E404D4003B2FB9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D29DF0CB21E404D4003B2FB9; + remoteInfo = MVMCoreUI; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 01004F2F22721C3800991ECC /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; 0103B84D23D7E33A009C315C /* HeadlineBodyToggleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyToggleModel.swift; sourceTree = ""; }; @@ -777,6 +792,11 @@ 52B201D124081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableRadioButtonAndPaymentMethodModel.swift; sourceTree = ""; }; 582272092B1FC55F00F75BAE /* AccessibilityHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibilityHandler.swift; sourceTree = ""; }; 5822720A2B1FC55F00F75BAE /* RotorHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotorHandler.swift; sourceTree = ""; }; + 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MVMCoreUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUITests.swift; sourceTree = ""; }; + 583335622BF6509C001D90D7 /* UAD_page_model.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = UAD_page_model.json; sourceTree = ""; }; + 583335642BF6A5C3001D90D7 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; + 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = UAD_page_model_2.json; sourceTree = ""; }; 5846ABF52B4762A600FA6C76 /* PollingBehaviorModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PollingBehaviorModel.swift; sourceTree = ""; }; 5878F0A42BD7E68800ADE23D /* mvmcoreui.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui.xcconfig; sourceTree = ""; }; 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = mvmcoreui_dev.xcconfig; sourceTree = ""; }; @@ -1223,6 +1243,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 583335532BF64E77001D90D7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5833355A2BF64E77001D90D7 /* MVMCoreUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0C921E404D4003B2FB9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1507,6 +1535,33 @@ path = Accessibility; sourceTree = ""; }; + 583335572BF64E77001D90D7 /* MVMCoreUITests */ = { + isa = PBXGroup; + children = ( + 583335602BF65063001D90D7 /* JSON */, + 583335582BF64E77001D90D7 /* MVMCoreUITests.swift */, + 583335642BF6A5C3001D90D7 /* TestUtils.swift */, + ); + path = MVMCoreUITests; + sourceTree = ""; + }; + 583335602BF65063001D90D7 /* JSON */ = { + isa = PBXGroup; + children = ( + 583335612BF6506C001D90D7 /* Modelling */, + ); + path = JSON; + sourceTree = ""; + }; + 583335612BF6506C001D90D7 /* Modelling */ = { + isa = PBXGroup; + children = ( + 583335662BF6DCD0001D90D7 /* UAD_page_model_2.json */, + 583335622BF6509C001D90D7 /* UAD_page_model.json */, + ); + path = Modelling; + sourceTree = ""; + }; 8DD1E36C243B3CD900D8F2DF /* ThreeColumn */ = { isa = PBXGroup; children = ( @@ -2016,6 +2071,7 @@ isa = PBXGroup; children = ( D29DF0CE21E404D4003B2FB9 /* MVMCoreUI */, + 583335572BF64E77001D90D7 /* MVMCoreUITests */, D29DF0CD21E404D4003B2FB9 /* Products */, D29DF0E421E4F3C7003B2FB9 /* Frameworks */, ); @@ -2025,6 +2081,7 @@ isa = PBXGroup; children = ( D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */, + 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */, ); name = Products; sourceTree = ""; @@ -2601,6 +2658,24 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 583335552BF64E77001D90D7 /* MVMCoreUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5833355F2BF64E77001D90D7 /* Build configuration list for PBXNativeTarget "MVMCoreUITests" */; + buildPhases = ( + 583335522BF64E77001D90D7 /* Sources */, + 583335532BF64E77001D90D7 /* Frameworks */, + 583335542BF64E77001D90D7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5833355C2BF64E77001D90D7 /* PBXTargetDependency */, + ); + name = MVMCoreUITests; + productName = MVMCoreUITests; + productReference = 583335562BF64E77001D90D7 /* MVMCoreUITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */ = { isa = PBXNativeTarget; buildConfigurationList = D29DF0D421E404D4003B2FB9 /* Build configuration list for PBXNativeTarget "MVMCoreUI" */; @@ -2625,9 +2700,13 @@ D29DF0C321E404D4003B2FB9 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1320; ORGANIZATIONNAME = "Verizon Wireless"; TargetAttributes = { + 583335552BF64E77001D90D7 = { + CreatedOnToolsVersion = 15.4; + }; D29DF0CB21E404D4003B2FB9 = { CreatedOnToolsVersion = 10.1; LastSwiftMigration = 1010; @@ -2650,11 +2729,21 @@ projectRoot = ""; targets = ( D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */, + 583335552BF64E77001D90D7 /* MVMCoreUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 583335542BF64E77001D90D7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 583335672BF6DCD0001D90D7 /* UAD_page_model_2.json in Resources */, + 583335632BF6509C001D90D7 /* UAD_page_model.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0CA21E404D4003B2FB9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2673,6 +2762,15 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 583335522BF64E77001D90D7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 583335592BF64E77001D90D7 /* MVMCoreUITests.swift in Sources */, + 583335652BF6A5C3001D90D7 /* TestUtils.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; D29DF0C821E404D4003B2FB9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3246,6 +3344,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5833355C2BF64E77001D90D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D29DF0CB21E404D4003B2FB9 /* MVMCoreUI */; + targetProxy = 5833355B2BF64E77001D90D7 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ D29DF32821EE8736003B2FB9 /* Localizable.strings */ = { isa = PBXVariantGroup; @@ -3260,6 +3366,49 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 5833355D2BF64E77001D90D7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vzw.MVMCoreUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5833355E2BF64E77001D90D7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.vzw.MVMCoreUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; D29DF0D221E404D4003B2FB9 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5878F0A52BD7E6BE00ADE23D /* mvmcoreui_dev.xcconfig */; @@ -3452,6 +3601,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5833355F2BF64E77001D90D7 /* Build configuration list for PBXNativeTarget "MVMCoreUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5833355D2BF64E77001D90D7 /* Debug */, + 5833355E2BF64E77001D90D7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; D29DF0C621E404D4003B2FB9 /* Build configuration list for PBXProject "MVMCoreUI" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme b/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme new file mode 100644 index 00000000..97460f91 --- /dev/null +++ b/MVMCoreUI.xcodeproj/xcshareddata/xcschemes/MVMCoreUI.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift index 505e112d..028c9f22 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonGroupModel.swift @@ -79,4 +79,16 @@ public class ButtonGroupModel: ParentMoleculeModelProtocol { try container.encodeIfPresent(childWidthValue, forKey: .childWidthValue) try container.encodeIfPresent(childWidthPercentage, forKey: .childWidthPercentage) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return buttons.count == model.buttons.count + && surface == model.surface + && enabled == model.enabled + && alignment == model.alignment + && rowQuantityPhone == model.rowQuantityPhone + && rowQuantityTablet == model.rowQuantityTablet + && childWidthValue == model.childWidthValue + && childWidthPercentage == model.childWidthPercentage + } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 8cc0bdda..ffeb8c36 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -192,7 +192,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat return title == model.title && enabled == model.enabled && inverted == model.inverted - && action.isEqual(to: model.action) && accessibilityText == model.accessibilityText && accessibilityIdentifier == model.accessibilityIdentifier && accessibilityTraits == model.accessibilityTraits @@ -201,6 +200,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat && size == model.size && groupName == model.groupName && width == model.width + && action.isEqual(to: model.action) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift index 7697143e..4970ce24 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift @@ -125,5 +125,19 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { try container.encode(width, forKey: .width) try container.encode(height, forKey: .height) try container.encode(enabled, forKey: .enabled) + try container.encode(inverted, forKey: .inverted) + } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && disabledColor == model.disabledColor + && color == model.color + && degrees == model.degrees + && lineWidth == model.lineWidth + && width == model.width + && height == model.height + && enabled == model.enabled + && inverted == model.inverted } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift index 5079fc93..3a827cbe 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift @@ -61,4 +61,20 @@ case shouldMaskRecordedView case allowServerParameters } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return backgroundColor == model.backgroundColor + && image == model.image + && accessibilityText == model.accessibilityText + && fallbackImage == model.fallbackImage + && imageFormat == model.imageFormat + && width == model.width + && height == model.height + && contentMode == model.contentMode + && cornerRadius == model.cornerRadius + && clipsImage == model.clipsImage + && allowServerParameters == model.allowServerParameters + && shouldMaskRecordedView == model.shouldMaskRecordedView + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift index 1dbac837..a4db3038 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift @@ -53,8 +53,4 @@ open class LabelAttributeActionModel: LabelAttributeModel { guard super.isEqual(to: model), let model = model as? Self else { return false } return action.isEqual(to: model.action) } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - return super.isEqual(to: model) - } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index 6da10fcc..df60e6ad 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -81,4 +81,10 @@ return location == model.location && length == model.length } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return location == model.location + && length == model.length + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index e3d37cf4..20e9d501 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -137,20 +137,20 @@ import VDS guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && text == model.text - && accessibilityText == model.accessibilityText && textColor == model.textColor && fontStyle == model.fontStyle && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.isEqual(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable && numberOfLines == model.numberOfLines + && accessibilityText == model.accessibilityText && accessibilityTraits == model.accessibilityTraits && inverted == inverted && shouldMaskRecordedView == model.shouldMaskRecordedView + && attributes.isEqual(to: model.attributes) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { @@ -162,7 +162,6 @@ import VDS && fontName == model.fontName && fontSize == model.fontSize && textAlignment == model.textAlignment - && attributes.isVisuallyEquivalent(to: model.attributes) && html == model.html && hero == model.hero && makeWholeViewClickable == model.makeWholeViewClickable @@ -170,6 +169,7 @@ import VDS && accessibilityText == model.accessibilityText && accessibilityTraits == model.accessibilityTraits && inverted == inverted + && attributes.isVisuallyEquivalent(to: model.attributes) } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 14b0969d..e757fb3b 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -53,17 +53,6 @@ public class TitleLockupModel: ParentMoleculeModelProtocol { && alignment == model.alignment && inverted == model.inverted && backgroundColor == model.backgroundColor - && children.isEqual(to: model.children) - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return textAlignment == model.textAlignment - && subTitleColor == model.subTitleColor - && alignment == model.alignment - && inverted == model.inverted - && backgroundColor == model.backgroundColor - && children.isVisuallyEquivalent(to: model.children) } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift index 12201722..a060dc2e 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeSectionHeaderModel.swift @@ -68,4 +68,9 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(line, forKey: .line) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return line.matchExistence(with: model.line) + } } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index 9f9ef766..3ea8a458 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -118,7 +118,7 @@ open class TabsModel: MoleculeModelProtocol { && borderLine == model.borderLine && minWidth == model.minWidth && selectedIndex == model.selectedIndex - && tabs.isEqual(to: model.tabs) + && tabs == model.tabs } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 2d978f17..5f7b423c 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -91,15 +91,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor && fillContainer == model.fillContainer - && primaryButton.isEqual(to: model.primaryButton) - && secondaryButton.isEqual(to: model.secondaryButton) - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && fillContainer == model.fillContainer - && primaryButton.isVisuallyEquivalent(to: model.primaryButton) - && secondaryButton.isVisuallyEquivalent(to: model.secondaryButton) + && primaryButton.matchExistence(with: model.primaryButton) + && secondaryButton.matchExistence(with: model.secondaryButton) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift index 1d07a6f5..8100c72b 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/AccordionListItemModel.swift @@ -80,6 +80,14 @@ class AccordionListItemModel: MoleculeListItemModel { try container.encodeModelIfPresent(expandAction, forKey: .expandAction) try container.encodeModelIfPresent(collapseAction, forKey: .collapseAction) } + + override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return selected == model.selected + && hideLineWhenExpanded == model.hideLineWhenExpanded + && expandAction.matchExistence(with: model.expandAction) + && collapseAction.matchExistence(with: model.collapseAction) + } } extension AccordionListItemModel: PageBehaviorProtocolRequirer { diff --git a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift index bf629f7a..0128c41e 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/ListItemModel.swift @@ -140,7 +140,7 @@ import MVMCore && accessibilityText == model.accessibilityText && accessibilityValue == model.accessibilityValue && accessibilityTraits == model.accessibilityTraits - && line.isEqual(to: model.line) + && line.matchExistence(with: model.line) && action.isEqual(to: model.action) } @@ -153,6 +153,6 @@ import MVMCore && accessibilityText == model.accessibilityText && accessibilityValue == model.accessibilityValue && accessibilityTraits == model.accessibilityTraits - && line.isVisuallyEquivalent(to: model.line) + && line.matchExistence(with: model.line) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 9bfa69c0..945b4b49 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -82,6 +82,6 @@ public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return border == border + return border == model.border } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift index 82958a12..9e230607 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeListItemModel.swift @@ -52,14 +52,4 @@ import MVMCore try container.encode(moleculeName, forKey: .moleculeName) try container.encodeModel(molecule, forKey: .molecule) } - - public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard super.isEqual(to: model), let model = model as? Self else { return false } - return molecule.isEqual(to: model) - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return molecule.isVisuallyEquivalent(to: model.molecule) - } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift index 72b9ee24..1f17a3e7 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeStackItemModel.swift @@ -60,14 +60,7 @@ } public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { - guard super.isEqual(to: model), let model = model as? Self else { return false } - return spacing == model.spacing - && percent == model.percent - && gone == model.gone - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } + guard super.isEqual(to: model), let model = model as? Self else { return false } return spacing == model.spacing && percent == model.percent && gone == model.gone diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift index a769eb89..943f53ad 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsListItemModel.swift @@ -20,7 +20,7 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { private var addedMolecules: [ListItemModelProtocol & MoleculeModelProtocol]? public var children: [MoleculeModelProtocol] { - return molecules.flatMap { $0 } + return [tabs] + molecules.flatMap { $0 } } public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) throws -> MoleculeModelProtocol? { @@ -94,16 +94,7 @@ public class TabsListItemModel: ListItemModel, ParentMoleculeModelProtocol { public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard super.isEqual(to: model), let model = model as? Self else { return false } - return tabs.isEqual(to: model.tabs) - && molecules.count == model.molecules.count - && zip(molecules, model.molecules).allSatisfy({ $0.0.isEqual(to: $0.1) }) - } - - public override func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard super.isVisuallyEquivalent(to: model), let model = model as? Self else { return false } - return tabs.isVisuallyEquivalent(to: model.tabs) - && molecules.count == model.molecules.count - && zip(molecules, model.molecules).allSatisfy({ $0.0.isVisuallyEquivalent(to: $0.1) }) + return molecules.count == model.molecules.count } } diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift index ab1c993e..d30ba5d7 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift @@ -73,6 +73,25 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule try container.encodeIfPresent(imageRenderingMode, forKey: .imageRenderingMode) } + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return true } + return backgroundColor == model.backgroundColor + && accessibilityIdentifier == model.accessibilityIdentifier + && image == model.image + && accessibilityText == model.accessibilityText + && imageRenderingMode == model.imageRenderingMode + && action.isEqual(to: action) + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return true } + return backgroundColor == model.backgroundColor + && accessibilityIdentifier == model.accessibilityIdentifier + && image == model.image + && accessibilityText == model.accessibilityText + && imageRenderingMode == model.imageRenderingMode + } + //-------------------------------------------------- // MARK: - Method //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift index a31f801b..a97f4af6 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/MoleculeContainerModel.swift @@ -64,8 +64,7 @@ open class MoleculeContainerModel: ContainerModel, MoleculeContainerModelProtoco public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } - return molecule.isEqual(to: model.molecule) - && backgroundColor == model.backgroundColor + return backgroundColor == model.backgroundColor } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index bc9dad7f..14008a5d 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -110,6 +110,9 @@ public class EyebrowHeadlineBodyLinkModel: ParentMoleculeModelProtocol { public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && children.isEqual(to: model.children) + && eyebrow.matchExistence(with: model.eyebrow) + && headline.matchExistence(with: model.headline) + && body.matchExistence(with: model.body) + && link.matchExistence(with: model.link) } } diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index 1c2e03a7..80893c44 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -96,18 +96,8 @@ open class HeadlineBodyModel: ParentMoleculeModelProtocol { public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } - return headline.isEqual(to: model.headline) - && body.isEqual(to: model.body) - && style == style - && backgroundColor == backgroundColor - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return headline.isVisuallyEquivalent(to: model.headline) - && body.isVisuallyEquivalent(to: model.body) - && style == style - && backgroundColor == backgroundColor + return style == style + && backgroundColor == model.backgroundColor } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index acd44595..12399292 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -174,7 +174,7 @@ open class Carousel: View { MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)") if #available(iOS 15.0, *) { - if let originalModel, carouselModel.isVisuallyEquivalent(to: originalModel) { + if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel) { // Prevents a carousel reset while still updating the cell backing data through reconfigureItems. MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is visually equivalent. Skipping rebuild...") FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 1ef5b98f..42c8363c 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -175,14 +175,12 @@ import UIKit public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == backgroundColor - && molecules.isEqual(to: model.molecules) && spacing == model.spacing && border == model.border && loop == model.loop && height == model.height && itemWidthPercent == model.itemWidthPercent && itemAlignment == model.itemAlignment - && pagingMolecule.isEqual(to: model.pagingMolecule) && paging == model.paging && useHorizontalMargins == model.useHorizontalMargins && leftPadding == model.leftPadding @@ -193,6 +191,8 @@ import UIKit && groupName == model.groupName && enabled == model.enabled && readOnly == model.readOnly + && molecules.count == model.molecules.count + && pagingMolecule.matchExistence(with: model.pagingMolecule) } public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { @@ -212,8 +212,6 @@ import UIKit && baseValue == model.baseValue && enabled == model.enabled && readOnly == model.readOnly - && pagingMolecule.isVisuallyEquivalent(to: model.pagingMolecule) - && molecules.isVisuallyEquivalent(to: model.molecules) } } diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 65baa962..3b687c18 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -82,16 +82,8 @@ public func isEqual(to model: any ModelComparisonProtocol) -> Bool { guard let model = model as? Self else { return false } return backgroundColor == model.backgroundColor - && molecules.isEqual(to: model.molecules) - && axis == model.axis - && spacing == model.spacing - } - - public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { - guard let model = model as? Self else { return false } - return backgroundColor == model.backgroundColor - && molecules.isVisuallyEquivalent(to: model.molecules) && axis == model.axis && spacing == model.spacing + && molecules.count == model.molecules.count } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 5465c8e4..5e47e33f 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -11,7 +11,7 @@ import Foundation public protocol MoleculeModelComparisonProtocol: ModelComparisonProtocol { /** - True if there are no visual differences between models. + Shallow check if there are no visual differences between models. By default if the models are equal then they are visually equivalent. However, if there are parts of models that can be upddated without a UI update, this could be subset of properties. **/ @@ -25,6 +25,16 @@ extension MoleculeModelComparisonProtocol { } } +public extension MoleculeModelComparisonProtocol where Self: ParentModelProtocol { + func isDeeplyVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return !findFirst(in: model, failing: { mine, theirs in + guard let mine = mine as? MoleculeModelComparisonProtocol, let theirs = theirs as? MoleculeModelComparisonProtocol else { return false } + return mine.isVisuallyEquivalent(to: theirs) + }).matched + } +} + public extension Optional { /// Checks if the curent model is equal to another model. diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index bf55de18..3dc09d7c 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -21,7 +21,8 @@ public extension MoleculeModelProtocol { func isEqual(to model: ModelProtocol) -> Bool { guard let model = model as? Self else { return false } - return id == model.id + return moleculeName == model.moleculeName + && backgroundColor == model.backgroundColor } var debugDescription: String { diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 25dbcdc7..88b09b86 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -67,16 +67,6 @@ public protocol ParentMoleculeModelProtocol: ParentModelProtocol, MoleculeModelP public extension ParentMoleculeModelProtocol { - func isEqual(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return model.children.isEqual(to: model.children) - } - - func areVisuallyEquivalent(to model: any ModelProtocol) -> Bool { - guard let model = model as? Self else { return false } - return model.children.isVisuallyEquivalent(to: model.children) - } - func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int) -> Result) -> Result { var result = initialResult if (options == .parentFirst) { @@ -122,53 +112,66 @@ public extension ParentMoleculeModelProtocol { } } -extension ParentModelProtocol { +public extension ParentModelProtocol { - public typealias DeepCompareResult = (matched: Bool, myChild: ModelProtocol?, theirChild: ModelProtocol?) + typealias DeepCompareResult = (matched: Bool, mine: ModelProtocol?, theirs: ModelProtocol?) - public typealias ModelPair = (mine: ModelProtocol, theirs: ModelProtocol) + typealias ModelPair = (mine: ModelProtocol, theirs: ModelProtocol) - public func deepEquals(to model: any ModelProtocol) -> DeepCompareResult { - guard let model = model as? ParentModelProtocol else { return (false, self, model) } - return findFirst(in: model) { $0.isEqual(to: $1) } + func deepEquals(to model: any ModelProtocol) -> Bool { + guard let model = model as? ParentModelProtocol else { return false } + return !findFirst(in: model, failing: { $0.isEqual(to: $1) }).matched } - func findFirst(in anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> DeepCompareResult { + func findFirst(in anotherParent: ParentModelProtocol, failing test: (ModelProtocol, ModelProtocol)->Bool, options: TreeTraversalOptions = .childFirst) -> DeepCompareResult { - guard test(self, anotherParent) else { return (false, myChild: self, theirChild: anotherParent)} + guard options != .parentFirst, test(self, anotherParent) else { return (true, mine: self, theirs: anotherParent)} let myChildren = children let theirChildren = anotherParent.children - guard myChildren.count == theirChildren.count else { return (false, myChild: self, theirChild: self) } + guard myChildren.count == theirChildren.count else { return (true, mine: self, theirs: anotherParent) } for index in myChildren.indices { if let myChild = myChildren[index] as? ParentModelProtocol { if let theirChild = theirChildren[index] as? ParentModelProtocol { - let result = myChild.findFirst(in: theirChild, where: test) - guard result.0 else { return result } + let result = myChild.findFirst(in: theirChild, failing: test) + guard !result.0 else { return result } } else { - return (false, myChild: myChild, theirChild: theirChildren[index]) + return (true, mine: myChild, theirs: theirChildren[index]) } } else if !test(myChildren[index], theirChildren[index]) { - return (false, myChild: myChildren[index], theirChild: theirChildren[index]) + return (true, mine: myChildren[index], theirs: theirChildren[index]) } } - return (true, nil, nil) + guard options == .childFirst, test(self, anotherParent) else { return (true, mine: self, theirs: anotherParent)} + + return (false, nil, nil) + } + + func findAllTheirsNotEqual(against anotherParent: ParentModelProtocol) -> [T] { + return deepCompare(against: anotherParent) { $0.isEqual(to: $1) }.compactMap { $0.theirs as? T } + } + + func findAllNotEqual(against anotherParent: ParentModelProtocol) -> [ModelPair] { + return deepCompare(against: anotherParent) { $0.isEqual(to: $1) } } func deepCompare(against anotherParent: ParentModelProtocol, where test: (ModelProtocol, ModelProtocol)->Bool) -> [ModelPair] { - guard test(self, anotherParent) else { return [(self, anotherParent)]} + var allDiffs = [ModelPair]() let myChildren = children let theirChildren = anotherParent.children guard myChildren.count == theirChildren.count else { return [(self, anotherParent)] } - var allDiffs = [ModelPair]() + if !test(self, anotherParent) { + allDiffs.append((self, anotherParent)) + } + for index in myChildren.indices { if let myChild = myChildren[index] as? ParentModelProtocol, let theirChild = theirChildren[index] as? ParentModelProtocol { - let childDiffs = myChild.deepCompare(against: theirChild, where: test) as [ModelPair] + let childDiffs = myChild.deepCompare(against: theirChild, where: test) allDiffs.append(contentsOf: childDiffs) } else if !test(myChildren[index], theirChildren[index]) { allDiffs.append((myChildren[index], theirChildren[index])) diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 2ee8a04a..b598041b 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -7,7 +7,7 @@ // -public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol { +public protocol TemplateModelProtocol: PageModelProtocol, ModelProtocol, MoleculeTreeTraversalProtocol, ParentModelProtocol, MoleculeModelComparisonProtocol { var template: String { get } var rootMolecules: [MoleculeModelProtocol] { get } /// Page rendering ID. Unique betwen JSON parses. diff --git a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift index 78c7d61b..da57579d 100644 --- a/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/BaseTemplateModel.swift @@ -121,4 +121,18 @@ import Foundation try container.encodeIfPresent(tabBarIndex, forKey: .tabBarIndex) try container.encode(shouldMaskScreenWhileRecording, forKey: .shouldMaskScreenWhileRecording) } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return pageType == model.pageType + && backgroundColor == model.backgroundColor + && screenHeading == model.screenHeading + && formRules?.count ?? 0 == model.formRules?.count ?? 0 + && navigationBar.matchExistence(with: model.navigationBar) + && tabBarHidden == model.tabBarHidden + && hideLeftPanel == model.hideLeftPanel + && hideRightPanel == model.hideRightPanel + && tabBarIndex == model.tabBarIndex + && shouldMaskScreenWhileRecording == model.shouldMaskScreenWhileRecording + } } diff --git a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift index 5671013b..9af7b41d 100644 --- a/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift +++ b/MVMCoreUI/Atomic/Templates/ListPageTemplateModel.swift @@ -100,4 +100,14 @@ try container.encodeIfPresent(footerlessSpacerColor, forKey: .footerlessSpacerColor) try container.encodeIfPresent(footerlessSpacerHeight, forKey: .footerlessSpacerHeight) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return line.matchExistence(with: model.line) + && scrollToRowIndex == model.scrollToRowIndex + && singleCellSelection == model.singleCellSelection + && footerlessSpacerColor == model.footerlessSpacerColor + && footerlessSpacerHeight == model.footerlessSpacerHeight + && molecules?.count ?? 0 == model.molecules?.count ?? 0 + } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index ef382c0f..197701b0 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -270,6 +270,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol return IndexPath(row: $0, section: 0) } + debugLog("Refreshing rows \(indexPaths.map { $0.row })") + if #available(iOS 15.0, *) { // All rows should have been layed out already on the first newDataBuildScreen reload with the getMoleculeInfoList call. Therefore, we can be safe to assume the top level cell configuration will not be modified and only the child content will be updated allowing us to levearage this more efficient method. tableView.reconfigureRows(at: indexPaths) diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift index 921e74ab..481f8cad 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerModelBase.swift @@ -77,4 +77,12 @@ try container.encodeIfPresent(anchorFooter, forKey: .anchorFooter) try container.encodeModelIfPresent(footer, forKey: .footer) } + + public override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return anchorHeader == model.anchorHeader + && anchorFooter == model.anchorFooter + && header.matchExistence(with: model.header) + && footer.matchExistence(with: model.footer) + } } diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 56b58b89..50aa1df2 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -40,7 +40,7 @@ import MVMCore public var needsUpdateUI = false private var observingForResponses: NSObjectProtocol? private var initialLoadFinished = false - private var isFirstRender = true + public var isFirstRender = true public var previousScreenSize = CGSize.zero public var selectedField: UIView? @@ -259,7 +259,12 @@ import MVMCore if let updatedMolecules = behavior.onPageNew(rootMolecules: newTemplateModel.rootMolecules, delegateObjectIVar) { updatedMolecules.forEach { molecule in if let replaced = try? newTemplateModel.replaceChildMolecule(with: molecule) { - if !replaced.isEqual(to: molecule) { // Only recognize the molecules that actually changed. + // Only recognize the molecules that actually changed. + if let replaced = replaced as? ParentMoleculeModelProtocol, let molecule = molecule as? ParentMoleculeModelProtocol { + let diffs: [MoleculeModelProtocol] = replaced.findAllTheirsNotEqual(against: molecule) + debugLog("Behavior updated \(diffs) in template model.") + behaviorUpdatedModels.append(contentsOf: diffs) + } else if !replaced.isEqual(to: molecule) { debugLog("Behavior updated \(molecule) in template model.") behaviorUpdatedModels.append(molecule) // Need to specifically trace molecule updates here as replacements are modifying the original tree. (We don't have a deep copy.) } @@ -289,7 +294,7 @@ import MVMCore !new.isEqual(to: old) } debugLog("Page molecule updates\n\(diffs.map {"\($0.mine) vs. \($0.theirs)"}.joined(separator: "\n"))") - pageUpdatedModels = diffs.compactMap { $0.mine as? MoleculeModelProtocol } + pageUpdatedModels = diffs.compactMap { $0.theirs as? MoleculeModelProtocol } } let allUpdatedMolecules = isFirstRender ? [] : behaviorUpdatedModels + pageUpdatedModels @@ -303,7 +308,7 @@ import MVMCore debugLog("Performing full page render...") updateUI() } else { - debugLog("Updating \(allUpdatedMolecules) molecules...") + debugLog("Performing partial render of \(allUpdatedMolecules) molecules...") updateUI(for: allUpdatedMolecules) } diff --git a/MVMCoreUITests/JSON/Modelling/UAD_page_model.json b/MVMCoreUITests/JSON/Modelling/UAD_page_model.json new file mode 100644 index 00000000..aa880d36 --- /dev/null +++ b/MVMCoreUITests/JSON/Modelling/UAD_page_model.json @@ -0,0 +1,807 @@ +{ + "template": "list", + "analyticsData": { + "vzdl.user.customerBusiness": "joint_cust", + "vzdl.target.engagement.intent": "account management", + "vzdl.page.feedCardImpression": "L1|P1|greetingSection|Edit profile & settings^L1|P1|mobileAccountSection|BillsTile^L1|P2|mobileAccountSection|UsageTile^L1|P3|mobileAccountSection|OrdersTile^L1|P4|mobileAccountSection|PlansTile^L1|P5|mobileAccountSection|Services_perksTile^L1|P6|mobileAccountSection|Account_activityTile^L1|P1|fivegHomeAccountSection|BillsTile^L1|P2|fivegHomeAccountSection|OrdersTile^L1|P3|fivegHomeAccountSection|PlansTile^L1|P4|fivegHomeAccountSection|Account_activityTile^L1|P5|fivegHomeAccountSection|Services_perksTile^L1|P1|fiosAccountSection|BillsTile^L1|P2|fiosAccountSection|PlansTile^L1|P3|fiosAccountSection|SupportTile^L1|P4|fiosAccountSection|Account_activityTile^L1|P5|fiosAccountSection|Home_offersTile", + "vzdl.user.id": "a3b00d5af5c7b5d26d017023f12dfa791dba533a33a3ed264c8c98237ae5902f", + "vzdl.user.account": "c0d793b51b000cce8d6ec062a71a224d0faaf8e50c36ebd31240539b31aca001", + "vzdl.user.accountType": "postpaid", + "vzdl.events.uadcardserved": "1", + "vzdl.page.channelSession": "4154e060-85ae-4e8f-a371-fb00c618797a", + "vzdl.page.siteSection": "mva_atomic", + "vzdl.page.sourceChannel": "mva", + "vzdl.page.flow": "account overview", + "vzdl.env.businessUnit": "wireless", + "vzdl.page.displayChannel": "mva", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.id": "atomicAccountLanding" + }, + "molecules": [ + { + "useVerticalMargins": false, + "backgroundColor": "black", + "moleculeName": "tabsListItem", + "tabs": { + "borderLine": false, + "moleculeName": "tabs", + "tabs": [ + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Mobile" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "5G Home" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Fios" + } + } + ], + "style": "dark", + "selectedIndex": 1 + }, + "molecules": [ + [ + { + "id": "priorityTiles", + "line": { + "type": "none" + }, + "backgroundColor": "black", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 160, + "molecules": [ + { + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "topPadding": 16, + "useHorizontalMargins": false, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + } + ], + "itemWidthPercent": 100, + "accessibilityText": "carousel", + "spacing": 12, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20, + "inverted": true + }, + "rightPadding": 16, + "leftPadding": 16 + }, + "gone": true + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Bills", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|mobileAccountSection|BillsTile", + "vzdl.page.linkName": "Mobile|Bills", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/bill/overview/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Usage", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|mobileAccountSection|UsageTile", + "vzdl.page.linkName": "Mobile|Usage", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "sourcePage": "plansAndDeviceLanding", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "dataUsageDetails" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Orders", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|mobileAccountSection|OrdersTile", + "vzdl.page.linkName": "Mobile|Orders", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/orders/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Plans", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P4|mobileAccountSection|PlansTile", + "vzdl.page.linkName": "Mobile|Plans", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/cpc/mvm?pmd=y&tabbar=true", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Services & perks", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P5|mobileAccountSection|Services_perksTile", + "vzdl.page.linkName": "Mobile|Services & perks", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/products/producthub/home", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "id": "notification_count", + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Account activity", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P6|mobileAccountSection|Account_activityTile", + "vzdl.page.linkName": "Mobile|Account activity", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/optg/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "moleculeName": "listItem", + "line": { + "moleculeName": "line", + "type": "none" + }, + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Devices", + "fontStyle": "BoldTitleMedium" + }, + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Manage all devices", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/devices/landing/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Manage all devices" + }, + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "moleculeName": "listItem", + "style": "none", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "moleculeName": "carouselItem", + "molecule": { + "molecule": { + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Add a line and get select phones on us", + "fontStyle": "RegularMicro" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularMicro", + "numberOfLines": 1, + "moleculeName": "label", + "text": "\n\n\n\n\n" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Shop", + "moleculeName": "button", + "action": { + "extraParameters": { + "browserUrl": "sales/next/shop.html?flow=AAL&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "center" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Bring my own", + "moleculeName": "link", + "action": { + "extraParameters": { + "browserUrl": "sales/digital/byod.html?flow=NSO&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "fill", + "topPadding": 12, + "image": { + "image": "https://ss7.vzw.com/is/image/VerizonWireless/background-image-mobile?&scl=2", + "moleculeName": "image" + }, + "moleculeName": "bgImageContainer", + "bottomPadding": 12 + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 48, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "label", + "text": "Make the most of your plans", + "fontStyle": "BoldTitleMedium" + } + }, + { + "id": "forYouTiles", + "style": "none", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Discover more", + "fontStyle": "BoldTitleMedium" + }, + "verticalAlignment": "center", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Mobile|Shop all", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "locale": "EN", + "browserUrl": "sales/digital/shoplanding.html?isShopFlow=true&entrypoint=tabbar", + "requestFrom": "Shop" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Shop all" + }, + "verticalAlignment": "center", + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "id": "discoverTiles", + "style": "none", + "moleculeName": "listItem", + "bottomPadding": 32, + "useVerticalMargins": true, + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + } + ] + ] + } + ], + "backgroundColor": "black", + "hab": { + "configuration": "single", + "inverted": false + }, + "cache": true, + "header": { + "useVerticalMargins": true, + "backgroundColor": "black", + "moleculeName": "stack", + "bottomPadding": 12, + "molecules": [ + { + "moleculeName": "stackItem", + "id": "greetingSection", + "molecule": { + "fontStyle": "RegularTitleMedium", + "textColor": "#FFFFFF", + "moleculeName": "label", + "text": "Hi Lebowski." + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "enabledColor": "white", + "title": "Edit profile & settings", + "moleculeName": "link", + "inverted": true, + "action": { + "actionType": "openPage", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|greetingSection|Edit profile & settings", + "vzdl.page.linkName": "Edit profile & settings", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "pageType": "oneVzIdSettingsLanding", + "presentationStyle": "push", + "requestURL": "https://mobile-exp-qa2.vzw.com/mobile/nsa/nos/gw/launchapp/l2/webview", + "tryToReplaceFirst": false, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/acct/unifiedprofile/overview" + } + } + } + } + ] + }, + "behaviors": [ + { + "moleculeIds": [ + "priorityTiles", + "forYouTiles", + "discoverTiles", + "priorityTiles5G", + "forYouTiles5G", + "discoverTiles5G", + "notification_count", + "priorityTilesFIOS", + "phoneServiceFIOS", + "internetServiceFIOS", + "tvServiceFIOS", + "forYouTilesFIOS", + "discoverTilesFIOS", + "FiosBills", + "FiosSubBills", + "FiosPlans", + "FiosSupport", + "notification_count_fios", + "greetingSection", + "FiosHomeOffers", + "priorityTilesLTE", + "forYouTilesLTE", + "discoverTilesLTE" + ], + "behaviorName": "replaceMoleculeBehavior" + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/topCardsMVA", + "pageType": "topCardsMVA" + } + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/bottomCardsMVA", + "pageType": "bottomCardsMVA" + } + } + ], + "pageType": "atomicAccountLanding", + "loggedInMdn": "2815434851", + "presentationStyle": "root", + "footerlessSpacerColor": "white", + "tabBarIndex": 1, + "navigationBar": { + "style": "dark", + "moleculeName": "navigationBar", + "additionalLeftButtons": [ + { + "accessibilityText": "Verizon logo button, tap anytime to scroll to top of page", + "moleculeName": "navigationImageButton", + "image": "nav_vz_mark", + "imageRenderingMode": "alwaysOriginal", + "action": { + "actionType": "noop" + } + } + ], + "additionalRightButtons": [ + { + "accessibilityText": "Stores", + "moleculeName": "navigationImageButton", + "image": "nav_stores_white", + "action": { + "analyticsData": { + "vzdl.page.linkName": "global tab nav:stores" + }, + "title": "Stores", + "actionType": "openPage", + "pageType": "rtlStoreJourney" + } + } + ] + }, + "footerlessSpacerHeight": 0 +} diff --git a/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json b/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json new file mode 100644 index 00000000..3d292f72 --- /dev/null +++ b/MVMCoreUITests/JSON/Modelling/UAD_page_model_2.json @@ -0,0 +1,807 @@ +{ + "template": "list", + "analyticsData": { + "vzdl.user.customerBusiness": "joint_cust", + "vzdl.target.engagement.intent": "account management", + "vzdl.page.feedCardImpression": "L1|P1|greetingSection|Edit profile & settings^L1|P1|mobileAccountSection|BillsTile^L1|P2|mobileAccountSection|UsageTile^L1|P3|mobileAccountSection|OrdersTile^L1|P4|mobileAccountSection|PlansTile^L1|P5|mobileAccountSection|Services_perksTile^L1|P6|mobileAccountSection|Account_activityTile^L1|P1|fivegHomeAccountSection|BillsTile^L1|P2|fivegHomeAccountSection|OrdersTile^L1|P3|fivegHomeAccountSection|PlansTile^L1|P4|fivegHomeAccountSection|Account_activityTile^L1|P5|fivegHomeAccountSection|Services_perksTile^L1|P1|fiosAccountSection|BillsTile^L1|P2|fiosAccountSection|PlansTile^L1|P3|fiosAccountSection|SupportTile^L1|P4|fiosAccountSection|Account_activityTile^L1|P5|fiosAccountSection|Home_offersTile", + "vzdl.user.id": "a3b00d5af5c7b5d26d017023f12dfa791dba533a33a3ed264c8c98237ae5902f", + "vzdl.user.account": "c0d793b51b000cce8d6ec062a71a224d0faaf8e50c36ebd31240539b31aca001", + "vzdl.user.accountType": "postpaid", + "vzdl.events.uadcardserved": "1", + "vzdl.page.channelSession": "4154e060-85ae-4e8f-a371-fb00c618797a", + "vzdl.page.siteSection": "mva_atomic", + "vzdl.page.sourceChannel": "mva", + "vzdl.page.flow": "account overview", + "vzdl.env.businessUnit": "wireless", + "vzdl.page.displayChannel": "mva", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.id": "atomicAccountLanding" + }, + "molecules": [ + { + "useVerticalMargins": false, + "backgroundColor": "black", + "moleculeName": "tabsListItem", + "tabs": { + "borderLine": false, + "moleculeName": "tabs", + "tabs": [ + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Mobile" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "5G Home" + } + }, + { + "label": { + "fontStyle": "BoldTitleSmall", + "textColor": "white", + "moleculeName": "label", + "text": "Fios" + } + } + ], + "style": "dark", + "selectedIndex": 1 + }, + "molecules": [ + [ + { + "id": "priorityTiles", + "line": { + "type": "none" + }, + "backgroundColor": "black", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 160, + "molecules": [ + { + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "topPadding": 16, + "useHorizontalMargins": false, + "cornerRadius": 8, + "useVerticalMargins": true, + "backgroundColor": "white", + "bottomPadding": 16 + } + ], + "itemWidthPercent": 100, + "accessibilityText": "carousel", + "spacing": 12, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20, + "inverted": true + }, + "rightPadding": 16, + "leftPadding": 16 + }, + "gone": true + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Bills", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|mobileAccountSection|BillsTile", + "vzdl.page.linkName": "Mobile|Bills", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/bill/overview/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Usage", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P2|mobileAccountSection|UsageTile", + "vzdl.page.linkName": "Mobile|Usage", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "2", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "sourcePage": "plansAndDeviceLanding", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobileFirstSS", + "pageType": "dataUsageDetails" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Orders", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P3|mobileAccountSection|OrdersTile", + "vzdl.page.linkName": "Mobile|Orders", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/orders/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Plans", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P4|mobileAccountSection|PlansTile", + "vzdl.page.linkName": "Mobile|Plans", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/cpc/mvm?pmd=y&tabbar=true", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Services & perks", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P5|mobileAccountSection|Services_perksTile", + "vzdl.page.linkName": "Mobile|Services & perks", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/products/producthub/home", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "id": "notification_count", + "headlineBody": { + "moleculeName": "headlineBody", + "headline": { + "moleculeName": "label", + "text": "Account activity", + "textColor": "#000000" + } + }, + "backgroundColor": "#FFFFFF", + "moleculeName": "list1CFWBdy", + "action": { + "actions": [ + { + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P6|mobileAccountSection|Account_activityTile", + "vzdl.page.linkName": "Mobile|Account activity", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/optg/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + ], + "actionType": "actions" + } + }, + { + "moleculeName": "listItem", + "line": { + "moleculeName": "line", + "type": "none" + }, + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Devices", + "fontStyle": "BoldTitleMedium" + }, + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Manage all devices", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/devices/landing/", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Manage all devices" + }, + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "moleculeName": "listItem", + "style": "none", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "moleculeName": "carouselItem", + "molecule": { + "molecule": { + "moleculeName": "stack", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Add a line and get select phones on us", + "fontStyle": "RegularMicro" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "fontStyle": "RegularMicro", + "numberOfLines": 1, + "moleculeName": "label", + "text": "\n\n\n\n\n" + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Shop", + "moleculeName": "button", + "action": { + "extraParameters": { + "browserUrl": "sales/next/shop.html?flow=AAL&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "verticalAlignment": "trailing", + "horizontalAlignment": "center" + }, + { + "moleculeName": "stackItem", + "molecule": { + "size": "small", + "title": "Bring my own", + "moleculeName": "link", + "action": { + "extraParameters": { + "browserUrl": "sales/digital/byod.html?flow=NSO&fromAcct=true&isShopFlow=true&entrypoint=account", + "requestFrom": "UAD" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + } + }, + "horizontalAlignment": "center" + } + ] + }, + "verticalAlignment": "fill", + "topPadding": 12, + "image": { + "image": "https://ss7.vzw.com/is/image/VerizonWireless/background-image-mobile?&scl=2", + "moleculeName": "image" + }, + "moleculeName": "bgImageContainer", + "bottomPadding": 12 + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 48, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "label", + "text": "Make the most of your plans", + "fontStyle": "BoldTitleMedium" + } + }, + { + "id": "forYouTiles", + "style": "none", + "moleculeName": "listItem", + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + }, + { + "line": { + "moleculeName": "line", + "type": "none" + }, + "moleculeName": "listItem", + "style": "shortDivider", + "molecule": { + "moleculeName": "stack", + "axis": "horizontal", + "molecules": [ + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "label", + "text": "Discover more", + "fontStyle": "BoldTitleMedium" + }, + "verticalAlignment": "center", + "horizontalAlignment": "leading" + }, + { + "moleculeName": "stackItem", + "molecule": { + "moleculeName": "link", + "action": { + "analyticsData": { + "vzdl.page.id": "atomicAccountLanding", + "vzdl.page.name": "atomicAccountLanding", + "vzdl.page.displayChannel": "mva", + "vzdl.page.linkName": "Mobile|Shop all", + "vzdl.page.sourceChannel": "mva" + }, + "extraParameters": { + "locale": "EN", + "browserUrl": "sales/digital/shoplanding.html?isShopFlow=true&entrypoint=tabbar", + "requestFrom": "Shop" + }, + "actionType": "openPage", + "appContext": "mobile/nsa/nos/gw/launchapp/l2", + "pageType": "webview" + }, + "title": "Shop all" + }, + "verticalAlignment": "center", + "horizontalAlignment": "trailing" + } + ] + } + }, + { + "id": "discoverTiles", + "style": "none", + "moleculeName": "listItem", + "bottomPadding": 32, + "useVerticalMargins": true, + "molecule": { + "moleculeName": "carousel", + "height": 230, + "molecules": [ + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + }, + { + "useHorizontalMargins": false, + "useVerticalMargins": false, + "backgroundColor": "coolGray1", + "moleculeName": "carouselItem", + "molecule": { + "contentMode": "scaleToFill", + "moleculeName": "image", + "image": "single_card_skeleton_loader", + "imageFormat": "gif" + }, + "cornerRadius": 8 + } + ], + "itemWidthPercent": 45, + "accessibilityText": "carousel", + "spacing": 16, + "pagingMolecule": { + "moleculeName": "barsCarouselIndicator", + "position": -20 + } + } + } + ] + ] + } + ], + "backgroundColor": "black", + "hab": { + "configuration": "single", + "inverted": false + }, + "cache": true, + "header": { + "useVerticalMargins": true, + "backgroundColor": "black", + "moleculeName": "stack", + "bottomPadding": 12, + "molecules": [ + { + "moleculeName": "stackItem", + "id": "greetingSection", + "molecule": { + "fontStyle": "RegularTitleMedium", + "textColor": "#FFFFFF", + "moleculeName": "label", + "text": "Hi Lebowski." + } + }, + { + "moleculeName": "stackItem", + "molecule": { + "enabledColor": "white", + "title": "Edit profile & settings", + "moleculeName": "link", + "inverted": true, + "action": { + "actionType": "openPage", + "analyticsData": { + "vzdl.page.sourceChannel": "mva", + "vzdl.page.feedCardClicked": "L1|P1|greetingSection|Edit profile & settings", + "vzdl.page.linkName": "Edit profile & settings", + "vzdl.page.displayChannel": "mva", + "vzdl.page.id": "atomicAccountLanding", + "vzdl.events.uadcardclicked": "1", + "vzdl.page.name": "atomicAccountLanding" + }, + "pageType": "oneVzIdSettingsLanding", + "presentationStyle": "push", + "requestURL": "https://mobile-exp-qa2.vzw.com/mobile/nsa/nos/gw/launchapp/l2/webview", + "tryToReplaceFirst": false, + "extraParameters": { + "browserUrl": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/ui/acct/unifiedprofile/overview" + } + } + } + } + ] + }, + "behaviors": [ + { + "moleculeIds": [ + "priorityTiles", + "forYouTiles", + "discoverTiles", + "priorityTiles5G", + "forYouTiles5G", + "discoverTiles5G", + "notification_count", + "priorityTilesFIOS", + "phoneServiceFIOS", + "internetServiceFIOS", + "tvServiceFIOS", + "forYouTilesFIOS", + "discoverTilesFIOS", + "FiosBills", + "FiosSubBills", + "FiosPlans", + "FiosSupport", + "notification_count_fios", + "greetingSection", + "FiosHomeOffers", + "priorityTilesLTE", + "forYouTilesLTE", + "discoverTilesLTE" + ], + "behaviorName": "replaceMoleculeBehavior" + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/topCardsMVA", + "pageType": "topCardsMVA" + } + }, + { + "refreshOnShown": true, + "moduleIds": [ + "priorityTiles", + "priorityTiles5G", + "priorityTilesLTE" + ], + "refreshOnFirstLoad": true, + "behaviorName": "pollingBehavior", + "runWhileHidden": false, + "refreshInterval": 10, + "refreshAction": { + "background": true, + "extraParameters": { + "category": "AccountOverview", + "channel": "VZW-MFA", + "locale": "EN", + "alwaysUseFallbackResponse": false, + "platform": "IOS", + "isLTE": false, + "requestFrom": "UAD", + "pageContext": "Account_Overview", + "isFWA": true + }, + "actionType": "openPage", + "requestURL": "https://vzwqa2.verizonwireless.com/digital/nsa/secure/gw/udb/bottomCardsMVA", + "pageType": "bottomCardsMVA" + } + } + ], + "pageType": "atomicAccountLanding", + "loggedInMdn": "2815434851", + "presentationStyle": "root", + "footerlessSpacerColor": "white", + "tabBarIndex": 1, + "navigationBar": { + "style": "dark", + "moleculeName": "navigationBar", + "additionalLeftButtons": [ + { + "accessibilityText": "Verizon logo button, tap anytime to scroll to top of page", + "moleculeName": "navigationImageButton", + "image": "nav_vz_mark", + "imageRenderingMode": "alwaysOriginal", + "action": { + "actionType": "noop" + } + } + ], + "additionalRightButtons": [ + { + "accessibilityText": "Stores", + "moleculeName": "navigationImageButton", + "image": "nav_stores_white", + "action": { + "analyticsData": { + "vzdl.page.linkName": "global tab nav:stores" + }, + "title": "Stores", + "actionType": "openPage", + "pageType": "rtlStoreJourney" + } + } + ] + }, + "footerlessSpacerHeight": 0 +} diff --git a/MVMCoreUITests/MVMCoreUITests.swift b/MVMCoreUITests/MVMCoreUITests.swift new file mode 100644 index 00000000..af631c93 --- /dev/null +++ b/MVMCoreUITests/MVMCoreUITests.swift @@ -0,0 +1,212 @@ +// +// MVMCoreUITests.swift +// MVMCoreUITests +// +// Created by Kyle Hedden on 5/16/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import XCTest +import MVMCoreUI +import MVMCore + +enum TestError: Error { + case resoureNotFound +} + +final class MVMCoreUITests: XCTestCase { + + override class func setUp() { + super.setUp() + CoreUIModelMapping.registerObjects() + } + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + //func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + //} + + func testModelShallowEquality() throws { + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + XCTAssertTrue(model1.isVisuallyEquivalent(to: model2)) + XCTAssertTrue(model1.isVisuallyEquivalent(to: model3)) + + XCTAssertTrue(LabelModel(text: "Hello World").isEqual(to: LabelModel(text: "Hello World"))) + XCTAssertFalse(LabelModel(text: "Hello World").isEqual(to: LabelModel(text: "Hello Moon"))) + } + + func testFindFirstCompare() throws { + + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + let result = model1.findFirst(in: model2) { model1, model2 in + model1.isEqual(to: model2) + } + XCTAssertTrue(result.matched) + XCTAssertEqual((result.theirs as? LabelModel)?.text, "Hello Moon") + } + + func testDeepCompare() throws { + + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + + let attributedLabel = LabelModel(text: "Hello World") + attributedLabel.attributes = [LabelAttributeActionModel(0, 5, action: ActionNoopModel())] + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + attributedLabel + ) + ]) + + let attributedLabel2 = LabelModel(text: "Hello World") + attributedLabel2.attributes = [LabelAttributeActionModel(0, 5, action: ActionBackModel())] + let model4 = StackModel(molecules: [ + MoleculeStackItemModel(with: + attributedLabel2 + ) + ]) + + let results = model1.findAllNotEqual(against: model2) + XCTAssertEqual(results.count, 1) + XCTAssertEqual((results.first?.theirs as? LabelModel)?.text, "Hello Moon") + XCTAssertFalse(model1.deepEquals(to: model2)) + XCTAssertFalse(model1.isDeeplyVisuallyEquivalent(to: model2)) + XCTAssertFalse(model1.deepEquals(to: model3)) + + let visualResults = model3.findFirst(in: model4, failing: { mine, theirs in + guard let mine = mine as? MoleculeModelComparisonProtocol, let theirs = theirs as? MoleculeModelComparisonProtocol else { return false } + return mine.isVisuallyEquivalent(to: theirs) + }) + XCTAssertFalse(visualResults.matched) + XCTAssertTrue(model3.isDeeplyVisuallyEquivalent(to: model4)) + } + + func testDeepCompareReturnsAllResults() { + let model1 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ) + ]) + ) + ]) + + let model2 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + ) + ]) + + let model3 = StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Stars") + ), + MoleculeStackItemModel(with: + StackModel(molecules: [ + MoleculeStackItemModel(with: + LabelModel(text: "Hello Moon") + ) + ]) + ), + MoleculeStackItemModel(with: + LabelModel(text: "Hello World") + ) + ]) + + var results = model1.findAllNotEqual(against: model2) + XCTAssertEqual(results.count, 3) + + results = model1.findAllNotEqual(against: model3) + XCTAssertEqual(results.count, 3) + } + + func testPageEquality() throws { + let listTemplateModel1 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel2 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel3 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_2")) + + let results = listTemplateModel1.findFirst(in: listTemplateModel2, failing: { $0.isEqual(to: $1) }) + XCTAssertFalse(results.matched) + XCTAssertTrue(listTemplateModel1.deepEquals(to: listTemplateModel2)) + XCTAssertTrue(listTemplateModel1.isDeeplyVisuallyEquivalent(to: listTemplateModel2)) + + let results2 = listTemplateModel1.findFirst(in: listTemplateModel3, failing: { $0.isEqual(to: $1) }) + XCTAssertTrue(results2.matched) + XCTAssertFalse(listTemplateModel1.deepEquals(to: listTemplateModel3)) + XCTAssertTrue(listTemplateModel1.isDeeplyVisuallyEquivalent(to: listTemplateModel3)) + } + + func testPageEqualityPerformance() throws { + let listTemplateModel1 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model")) + let listTemplateModel2 = try JSONDecoder().decode(ListPageTemplateModel.self, from: getFileData("UAD_page_model_2")) + measure { + XCTAssertFalse(listTemplateModel1.deepEquals(to: listTemplateModel2)) + } + } + +} diff --git a/MVMCoreUITests/TestUtils.swift b/MVMCoreUITests/TestUtils.swift new file mode 100644 index 00000000..576a3231 --- /dev/null +++ b/MVMCoreUITests/TestUtils.swift @@ -0,0 +1,19 @@ +// +// TestUtils.swift +// MVMCoreUITests +// +// Created by Kyle Hedden on 5/16/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import XCTest + +extension XCTestCase { + + func getFileData(_ fileName: String, type: String = "json") throws -> Data { + guard let resourcePath = Bundle(identifier: "com.vzw.MVMCoreUITests")?.path(forResource: fileName, ofType: type) else { throw TestError.resoureNotFound } + return try Data(contentsOf: URL(fileURLWithPath: resourcePath)) + } + +}