diff --git a/client/ios/DivKit.xcodeproj/project.pbxproj b/client/ios/DivKit.xcodeproj/project.pbxproj index 0053f4360..da44d6b73 100644 --- a/client/ios/DivKit.xcodeproj/project.pbxproj +++ b/client/ios/DivKit.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ 4391F7C32A77DB9B006D8818 /* DivViewController+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4391F7C22A77DB9B006D8818 /* DivViewController+SwiftUI.swift */; }; 52184FD8296C68E400ECC0E9 /* DivKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 52184FD7296C68E400ECC0E9 /* DivKitExtensions */; }; 5219194D297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */; }; + 52907F662A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */; }; + 52CD80672A7122AA00B828B0 /* DivBaseExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */; }; + 52CD80692A7159C200B828B0 /* DivStateExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */; }; + 52CD806B2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */; }; 52DED04F29F7349000D64519 /* DivKit in Frameworks */ = {isa = PBXBuildFile; productRef = 52DED04E29F7349000D64519 /* DivKit */; }; 52DED05129F7349000D64519 /* DivKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 52DED05029F7349000D64519 /* DivKitExtensions */; }; 52DED05329F734B900D64519 /* ShimmerStyleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DED03C29F6E3D500D64519 /* ShimmerStyleTests.swift */; }; @@ -260,6 +264,10 @@ 437B19D329C8A73300A4C467 /* MaskValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaskValidatorTests.swift; sourceTree = ""; }; 4391F7C22A77DB9B006D8818 /* DivViewController+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DivViewController+SwiftUI.swift"; sourceTree = ""; }; 5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RunLoopCardUpdateAggregatorTests.swift; sourceTree = ""; }; + 52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivLastVisibleBoundsCacheTests.swift; sourceTree = ""; }; + 52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivBaseExtensionsTests.swift; sourceTree = ""; }; + 52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivStateExtensionsTests.swift; sourceTree = ""; }; + 52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+ViewVisibilityCallCount.swift"; sourceTree = ""; }; 52DED03C29F6E3D500D64519 /* ShimmerStyleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerStyleTests.swift; sourceTree = ""; }; 52DED04729F7346900D64519 /* DivKitExtensionsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DivKitExtensionsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52EEC66D293E64C90030FB11 /* DivBlockModelingContextErrorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivBlockModelingContextErrorsTests.swift; sourceTree = ""; }; @@ -843,6 +851,7 @@ 8CB960E528883B8E00D16E47 /* Platform.swift */, 5219194C297B3974007DD5F4 /* RunLoopCardUpdateAggregatorTests.swift */, 8C4DC3E72892946F000C0963 /* RuntimeTestCase.swift */, + 52907F652A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift */, ); path = DivKitTests; sourceTree = ""; @@ -872,6 +881,9 @@ 8C7B1B252865C01C0036EF4C /* DivTabsExtensionsTests.swift */, 8C7B1B282865C01C0036EF4C /* DivTextExtensionsTests.swift */, 8C1CF625286B561F0016D0A1 /* Expected.swift */, + 52CD80662A7122AA00B828B0 /* DivBaseExtensionsTests.swift */, + 52CD80682A7159C200B828B0 /* DivStateExtensionsTests.swift */, + 52CD806A2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift */, ); path = Extensions; sourceTree = ""; @@ -1505,6 +1517,7 @@ 8C23F41529DDB3810069F3F7 /* EntityWithArrayOfEnumsTemplate.swift in Sources */, 8C23F43229DDB3810069F3F7 /* EntityWithStringArrayPropertyTemplate.swift in Sources */, 8C23F44829DDB3810069F3F7 /* StringEnumPropertyTests.swift in Sources */, + 52CD806B2A7172B400B828B0 /* XCTestCase+ViewVisibilityCallCount.swift in Sources */, 8C23F43329DDB3810069F3F7 /* EntityWithStringEnumProperty.swift in Sources */, 8C23F44729DDB3810069F3F7 /* StrictArrayTests.swift in Sources */, 8C23F43B29DDB3810069F3F7 /* ArrayTests.swift in Sources */, @@ -1513,6 +1526,7 @@ 8C1CF626286B561F0016D0A1 /* Expected.swift in Sources */, 7035529E28D4881300E237CF /* DivDeserializationErrorsTests.swift in Sources */, E945FC46293E1CA80066C3EA /* Utils.swift in Sources */, + 52907F662A71199B00518F50 /* DivLastVisibleBoundsCacheTests.swift in Sources */, 8C23F41729DDB3810069F3F7 /* EntityWithArrayOfExpressionsTemplate.swift in Sources */, 8C23F41D29DDB3810069F3F7 /* EntityWithComplexProperty.swift in Sources */, 8C7B1CA62865C01D0036EF4C /* TemplatesTests.swift in Sources */, @@ -1529,6 +1543,7 @@ 8C7B1BF32865C01C0036EF4C /* DivTabsExtensionsTests.swift in Sources */, 8C23F44429DDB3810069F3F7 /* OptionalStringEnumPropertyTests.swift in Sources */, 891672372A5F6F9300121131 /* DivPersistentValuesStorageTests.swift in Sources */, + 52CD80672A7122AA00B828B0 /* DivBaseExtensionsTests.swift in Sources */, 8C23F43F29DDB3810069F3F7 /* EntityProtocol.swift in Sources */, 52EEC66E293E64C90030FB11 /* DivBlockModelingContextErrorsTests.swift in Sources */, 8C23F42729DDB3810069F3F7 /* EntityWithOptionalStringEnumProperty.swift in Sources */, @@ -1559,6 +1574,7 @@ 8C4DC3E82892946F000C0963 /* RuntimeTestCase.swift in Sources */, 8C23F42D29DDB3810069F3F7 /* EntityWithSimpleProperties.swift in Sources */, 8C23F42B29DDB3810069F3F7 /* EntityWithRequiredProperty.swift in Sources */, + 52CD80692A7159C200B828B0 /* DivStateExtensionsTests.swift in Sources */, 8C23F44329DDB3810069F3F7 /* OptionalPropertyTests.swift in Sources */, 8C1DEBCE2A2F8277009237F1 /* DivStateManagementTests.swift in Sources */, 8C7B1BFA2865C01C0036EF4C /* DivDataExtensionsTests.swift in Sources */, diff --git a/client/ios/DivKit/DivBlockModelingContext.swift b/client/ios/DivKit/DivBlockModelingContext.swift index 0a3f1c6ee..44f7fb020 100644 --- a/client/ios/DivKit/DivBlockModelingContext.swift +++ b/client/ios/DivKit/DivBlockModelingContext.swift @@ -20,6 +20,7 @@ public struct DivBlockModelingContext { public var stateManager: DivStateManager public let blockStateStorage: DivBlockStateStorage public let visibilityCounter: DivVisibilityCounting + public let lastVisibleBoundsCache: DivLastVisibleBoundsCache public let imageHolderFactory: ImageHolderFactory public let highPriorityImageHolderFactory: ImageHolderFactory? public let divCustomBlockFactory: DivCustomBlockFactory @@ -30,6 +31,7 @@ public struct DivBlockModelingContext { private let variables: DivVariables public let layoutDirection: UserInterfaceLayoutDirection public let debugParams: DebugParams + public let scheduler: Scheduling public let playerFactory: PlayerFactory? public var childrenA11yDescription: String? public weak var parentScrollView: ScrollView? @@ -61,6 +63,7 @@ public struct DivBlockModelingContext { stateManager: DivStateManager, blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(), visibilityCounter: DivVisibilityCounting = DivVisibilityCounter(), + lastVisibleBoundsCache: DivLastVisibleBoundsCache = DivLastVisibleBoundsCache(), imageHolderFactory: ImageHolderFactory, highPriorityImageHolderFactory: ImageHolderFactory? = nil, divCustomBlockFactory: DivCustomBlockFactory = EmptyDivCustomBlockFactory(), @@ -71,6 +74,7 @@ public struct DivBlockModelingContext { variables: DivVariables = [:], playerFactory: PlayerFactory? = nil, debugParams: DebugParams = DebugParams(), + scheduler: Scheduling? = nil, childrenA11yDescription: String? = nil, parentScrollView: ScrollView? = nil, errorsStorage: DivErrorsStorage = DivErrorsStorage(errors: []), @@ -85,6 +89,7 @@ public struct DivBlockModelingContext { self.stateManager = stateManager self.blockStateStorage = blockStateStorage self.visibilityCounter = visibilityCounter + self.lastVisibleBoundsCache = lastVisibleBoundsCache self.imageHolderFactory = imageHolderFactory self.highPriorityImageHolderFactory = highPriorityImageHolderFactory self.divCustomBlockFactory = divCustomBlockFactory @@ -93,6 +98,7 @@ public struct DivBlockModelingContext { self.variables = variables self.playerFactory = playerFactory self.debugParams = debugParams + self.scheduler = scheduler ?? TimerScheduler() self.childrenA11yDescription = childrenA11yDescription self.parentScrollView = parentScrollView self.errorsStorage = errorsStorage diff --git a/client/ios/DivKit/DivKitComponents.swift b/client/ios/DivKit/DivKitComponents.swift index 4ed51d604..5a615d565 100644 --- a/client/ios/DivKit/DivKitComponents.swift +++ b/client/ios/DivKit/DivKitComponents.swift @@ -33,6 +33,7 @@ public final class DivKitComponents { public let urlHandler: DivUrlHandler public let variablesStorage: DivVariablesStorage public let visibilityCounter = DivVisibilityCounter() + public let lastVisibleBoundsCache = DivLastVisibleBoundsCache() public var updateCardSignal: Signal<[DivActionURLHandler.UpdateReason]> { updateCardPipe.signal } @@ -227,6 +228,7 @@ public final class DivKitComponents { stateManager: stateManagement.getStateManagerForCard(cardId: cardId), blockStateStorage: blockStateStorage, visibilityCounter: visibilityCounter, + lastVisibleBoundsCache: lastVisibleBoundsCache, imageHolderFactory: imageHolderFactory .withInMemoryCache(cachedImageHolders: cachedImageHolders), divCustomBlockFactory: divCustomBlockFactory, diff --git a/client/ios/DivKit/DivLastVisibleBoundsCache.swift b/client/ios/DivKit/DivLastVisibleBoundsCache.swift new file mode 100644 index 000000000..29c575d18 --- /dev/null +++ b/client/ios/DivKit/DivLastVisibleBoundsCache.swift @@ -0,0 +1,40 @@ +import CoreGraphics +import LayoutKit +import CommonCorePublic + +public final class DivLastVisibleBoundsCache { + private let rwLock = RWLock() + + private var storage: [UIElementPath: CGRect] = [:] + + public init() {} + + public func lastVisibleBounds(for path: UIElementPath) -> CGRect { + rwLock.read { + storage[path] ?? .zero + } + } + + public func updateLastVisibleBounds(for path: UIElementPath, bounds: CGRect) { + rwLock.write { + storage[path] = bounds + } + } + + public func dropVisibleBounds(forMatchingPrefix prefix: UIElementPath) { + let prefix = prefix.description + "/" + rwLock.write { + storage.forEach { + if ($0.key.description + "/").starts(with: prefix) { + storage[$0.key] = nil + } + } + } + } + + public func reset() { + rwLock.write { + storage.removeAll() + } + } +} diff --git a/client/ios/DivKit/Extensions/DivBase/DivBaseExtensions.swift b/client/ios/DivKit/Extensions/DivBase/DivBaseExtensions.swift index 2292d4158..b8e6a643c 100644 --- a/client/ios/DivKit/Extensions/DivBase/DivBaseExtensions.swift +++ b/client/ios/DivKit/Extensions/DivBase/DivBaseExtensions.swift @@ -21,6 +21,7 @@ extension DivBase { let visibility = resolveVisibility(expressionResolver) if visibility == .gone { + context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath) context.stateManager.setBlockVisibility(statePath: statePath, div: self, isVisible: false) return EmptyBlock.zeroSized } @@ -39,6 +40,7 @@ extension DivBase { let externalInsets = margins.makeEdgeInsets(context: context) if visibility == .invisible { + context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath) context.stateManager.setBlockVisibility(statePath: statePath, div: self, isVisible: false) block = applyExtensionHandlersAfterBaseProperties( to: block.addingEdgeInsets(externalInsets), @@ -72,6 +74,16 @@ extension DivBase { border: border.makeBlockBorder(with: expressionResolver), shadow: border.makeBlockShadow(with: expressionResolver), visibilityActions: visibilityActions.isEmpty ? nil : visibilityActions, + lastVisibleBounds: visibilityActions.isEmpty ? nil : Property( + getter: { context.lastVisibleBoundsCache.lastVisibleBounds(for: context.parentPath) }, + setter: { + context.lastVisibleBoundsCache.updateLastVisibleBounds( + for: context.parentPath, + bounds: $0 + ) + } + ), + scheduler: context.scheduler, tooltips: try makeTooltips(context: context) ) .addingTransform( diff --git a/client/ios/DivKit/Extensions/DivData/DivDataExtensions.swift b/client/ios/DivKit/Extensions/DivData/DivDataExtensions.swift index 67b30cb8c..84d1218e8 100644 --- a/client/ios/DivKit/Extensions/DivData/DivDataExtensions.swift +++ b/client/ios/DivKit/Extensions/DivData/DivDataExtensions.swift @@ -11,6 +11,10 @@ extension DivData: DivBlockModeling { throw DivBlockModelingError("DivData has no states", path: context.parentPath) } + if let previousRootState = getPreviousRootState(stateManager: stateManager) { + context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath + previousRootState.rawValue) + } + let stateId = String(state.stateId) let statePath = DivStatePath(rawValue: UIElementPath(stateId)) let div = state.div @@ -31,7 +35,6 @@ extension DivData: DivBlockModeling { ) return block .addingStateBlock( - stateId: stateId, ids: stateManager.getVisibleIds(statePath: statePath) ) .addingDebugInfo(context: divContext) @@ -53,6 +56,22 @@ extension DivData: DivBlockModeling { context.addError(level: .error, message: "DivData.State not found: \(stateId)") return states.first } + + private func getPreviousRootState(stateManager: DivStateManager) -> DivStateID? { + guard let item = stateManager.get(stateBlockPath: DivData.rootPath) else { + return nil + } + + switch item.previousState { + case .empty, .initial: + return nil + case let .withID(id): + if item.currentStateID != id { + return id + } + return nil + } + } } extension DivData { diff --git a/client/ios/DivKit/Extensions/DivStateExtensions.swift b/client/ios/DivKit/Extensions/DivStateExtensions.swift index df2114715..1f1a41072 100644 --- a/client/ios/DivKit/Extensions/DivStateExtensions.swift +++ b/client/ios/DivKit/Extensions/DivStateExtensions.swift @@ -46,6 +46,8 @@ extension DivState: DivBlockModeling { let previousState = getPreviousState(stateManagerItem: stateManagerItem), previousState.stateId != activeStateId, let previousDiv = previousState.div { + // state changed -> drop visibility cache for all children + context.lastVisibleBoundsCache.dropVisibleBounds(forMatchingPrefix: context.parentPath) previousBlock = try previousDiv.value.makeBlock( context: context.makeContextForState( id: id, @@ -99,7 +101,6 @@ extension DivState: DivBlockModeling { ), ] ).addingStateBlock( - stateId: activeStateId, ids: stateManager.getVisibleIds(statePath: activeStatePath) ) } diff --git a/client/ios/DivKitTests/DivKitTests.swift b/client/ios/DivKitTests/DivKitTests.swift index d42f5d615..f487606fb 100644 --- a/client/ios/DivKitTests/DivKitTests.swift +++ b/client/ios/DivKitTests/DivKitTests.swift @@ -1,7 +1,7 @@ import XCTest -@testable import DivKit import CommonCorePublic +@testable import DivKit import LayoutKit import NetworkingPublic import Serialization @@ -48,7 +48,8 @@ extension DivBlockModelingContext { static let `default` = DivBlockModelingContext() init( - blockStateStorage: DivBlockStateStorage = DivBlockStateStorage() + blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(), + scheduler: Scheduling? = nil ) { self.init( cardId: DivKitTests.cardId, @@ -56,6 +57,7 @@ extension DivBlockModelingContext { stateManager: DivStateManager(), blockStateStorage: blockStateStorage, imageHolderFactory: ImageHolderFactory(make: { _, _ in FakeImageHolder() }), + scheduler: scheduler, persistentValuesStorage: DivPersistentValuesStorage() ) } diff --git a/client/ios/DivKitTests/DivLastVisibleBoundsCacheTests.swift b/client/ios/DivKitTests/DivLastVisibleBoundsCacheTests.swift new file mode 100644 index 000000000..9ffbdab06 --- /dev/null +++ b/client/ios/DivKitTests/DivLastVisibleBoundsCacheTests.swift @@ -0,0 +1,50 @@ +import DivKit +import LayoutKit +import XCTest + +final class DivLastVisibleBoundsCacheTests: XCTestCase { + let cache = DivLastVisibleBoundsCache() + + func test_WhenCacheIsEmpty_RetrievesZeroFromCache() { + XCTAssertEqual(cache.lastVisibleBounds(for: "path"), .zero) + } + + func test_WhenUpdateLastVisibleBounds_RetrievesUpdatedValueFromCache() { + let rect = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10)) + + cache.updateLastVisibleBounds(for: "path", bounds: rect) + + XCTAssertEqual(cache.lastVisibleBounds(for: "path"), rect) + } + + func test_WhenReset_RetrievesZeroFromCache() { + let rect = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10)) + + cache.updateLastVisibleBounds(for: "path", bounds: rect) + cache.reset() + + XCTAssertEqual(cache.lastVisibleBounds(for: "path"), .zero) + } + + func test_WhenDropVisibleBoundsWithMatchingPrefix_ClearsAllChildPaths() { + let rect1 = CGRect(origin: CGPoint(x: 10, y: 10), size: CGSize(squareDimension: 10)) + let rect2 = CGRect(origin: .zero, size: CGSize(squareDimension: 10)) + let rect3 = CGRect(origin: .zero, size: CGSize(squareDimension: 20)) + let parentPath: UIElementPath = "path" + let parentPath2: UIElementPath = "path2" + let childPath: UIElementPath = parentPath + "child" + let nestedChildPath: UIElementPath = childPath + "nestedChild" + + cache.updateLastVisibleBounds(for: parentPath, bounds: rect1) + cache.updateLastVisibleBounds(for: parentPath2, bounds: rect1) + cache.updateLastVisibleBounds(for: childPath, bounds: rect2) + cache.updateLastVisibleBounds(for: nestedChildPath, bounds: rect3) + + cache.dropVisibleBounds(forMatchingPrefix: parentPath) + + XCTAssertEqual(cache.lastVisibleBounds(for: childPath), .zero) + XCTAssertEqual(cache.lastVisibleBounds(for: nestedChildPath), .zero) + XCTAssertEqual(cache.lastVisibleBounds(for: parentPath2), rect1) + XCTAssertEqual(cache.lastVisibleBounds(for: parentPath), .zero) + } +} diff --git a/client/ios/DivKitTests/Extensions/DivBaseExtensionsTests.swift b/client/ios/DivKitTests/Extensions/DivBaseExtensionsTests.swift new file mode 100644 index 000000000..3cd0f7f9f --- /dev/null +++ b/client/ios/DivKitTests/Extensions/DivBaseExtensionsTests.swift @@ -0,0 +1,86 @@ +@testable import LayoutKit + +import XCTest + +import CommonCorePublic +import DivKit +import NetworkingPublic + +final class DivBaseExtensionsTests: XCTestCase { + private let timer = TestTimerScheduler() + + func test_WhenCreatesBlockAfterItBeingGone_ReportsVisibility() throws { + try expectVisibilityActionsToRun( + forVisibleBlockFile: "div-text-visibility-actions-visible", + invisibleBlockFile: "div-text-visibility-actions-gone" + ) + } + + func test_WhenCreatesBlockAfterItBeingInvisible_ReportsVisibility() throws { + try expectVisibilityActionsToRun( + forVisibleBlockFile: "div-text-visibility-actions-visible", + invisibleBlockFile: "div-text-visibility-actions-invisible" + ) + } + + func test_WhenReusesBlockWithAnotherDivCardID_ExpectToCancelPreviousBlockTimers() throws { + let context = DivBlockModelingContext(scheduler: timer) + + let firstBlock = try makeBlock(fromFile: "div-text-visibility-actions-visible", context: context) + + // trigger visibility actions for first time + let rect = CGRect(origin: .zero, size: CGSize(squareDimension: 20)) + let view = firstBlock.makeBlockView() + view.frame = rect + view.layoutIfNeeded() + view.onVisibleBoundsChanged(from: rect, to: rect) + + let oldTimers = timer.timers + + let secondBlock = try makeBlock( + fromFile: "div-text-visibility-actions-visible", + context: DivBlockModelingContext( + cardId: "another_cardId", + stateManager: DivStateManager(), + imageHolderFactory: ImageHolderFactory(make: { _, _ in FakeImageHolder() }) + ) + ) + let view2 = secondBlock.reuse(view, observer: nil, overscrollDelegate: nil, renderingDelegate: nil, superview: nil) + view2.frame = rect + view2.layoutIfNeeded() + view.onVisibleBoundsChanged(from: rect, to: rect) + + XCTAssertEqual(oldTimers.allSatisfy { !$0.isValid }, true) + } + + private func expectVisibilityActionsToRun( + forVisibleBlockFile file: String, + invisibleBlockFile: String + ) throws { + let context = DivBlockModelingContext(scheduler: timer) + + let blockVisibleFirst = try makeBlock(fromFile: file, context: context) + + // trigger visibility actions for first time + let rect = CGRect(origin: .zero, size: CGSize(squareDimension: 20)) + let view = blockVisibleFirst.makeBlockView() + XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timer), 1) + + // expect to drop lastVisibleBounds + let _ = try makeBlock(fromFile: invisibleBlockFile, context: context) + let view2 = blockVisibleFirst.makeBlockView() + + XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timer), 1) + } +} + +private func makeBlock( + fromFile filename: String, + context: DivBlockModelingContext = .default +) throws -> Block { + try DivTextTemplate.make( + fromFile: filename, + subdirectory: "div-base", + context: context + ) +} diff --git a/client/ios/DivKitTests/Extensions/DivDataExtensionsTests.swift b/client/ios/DivKitTests/Extensions/DivDataExtensionsTests.swift index 60d515d75..d84e03fce 100644 --- a/client/ios/DivKitTests/Extensions/DivDataExtensionsTests.swift +++ b/client/ios/DivKitTests/Extensions/DivDataExtensionsTests.swift @@ -60,6 +60,26 @@ final class DivDataExtensionsTests: XCTestCase { let expectedPath = UIElementPath.root + "0" + DivGallery.type XCTAssertEqual(galleryBlock?.model.path, expectedPath) } + + func test_WhenStateChanges_ReportsVisibilityForNewState() throws { + let timerScheduler = TestTimerScheduler() + let context = DivBlockModelingContext(scheduler: timerScheduler) + + for rootStateId in [nil, "1", "0", "1"] { + if let rootStateId { + context.stateManager.setStateWithHistory(path: DivData.rootPath, stateID: DivStateID(rawValue: rootStateId)) + } + + let block = try makeBlock(fromFile: "root_states_visibility", context: context) + + let rect = CGRect( + origin: .zero, + size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100)) + ) + let view = block.makeBlockView() + XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1) + } + } } private let data = makeDivData( @@ -103,3 +123,15 @@ extension Block { return self } } + +private func makeBlock( + fromFile filename: String, + context: DivBlockModelingContext = .default +) throws -> Block { + try DivDataTemplate.make( + fromFile: filename, + subdirectory: "div-data", + context: context + ) +} + diff --git a/client/ios/DivKitTests/Extensions/DivStateExtensionsTests.swift b/client/ios/DivKitTests/Extensions/DivStateExtensionsTests.swift new file mode 100644 index 000000000..07ee73c5f --- /dev/null +++ b/client/ios/DivKitTests/Extensions/DivStateExtensionsTests.swift @@ -0,0 +1,66 @@ +@testable import LayoutKit + +import XCTest + +import CommonCorePublic +import DivKit + +final class DivStateExtensionsTests: XCTestCase { + private let timerScheduler = TestTimerScheduler() + func test_WhenVisibilityWasTriggeredAndStateChanges_ReportsVisibilityAgain() throws { + let context = DivBlockModelingContext(scheduler: timerScheduler) + + let block = try makeBlock(fromFile: "states_visibility", context: context) + let rect = CGRect( + origin: .zero, + size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100)) + ) + let view = block.makeBlockView() + XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1) + + context.stateManager.setState(stateBlockPath: "mystate", stateID: "second") + + let block2 = try makeBlock(fromFile: "states_visibility", context: context) + let view2 = block2.reuse( + view, + observer: nil, + overscrollDelegate: nil, + renderingDelegate: nil, + superview: nil + ) + XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timerScheduler), 1) + } + + func test_WhenVisibilityWasTriggeredAndStateDoesNotChange_DoesNotReportVisibilityAgain() throws { + let context = DivBlockModelingContext(scheduler: timerScheduler) + + let block = try makeBlock(fromFile: "states_visibility", context: context) + let rect = CGRect( + origin: .zero, + size: CGSize(width: 100, height: block.intrinsicContentHeight(forWidth: 100)) + ) + let view = block.makeBlockView() + XCTAssertEqual(getViewVisibilityCallCount(view: view, rect: rect, timerScheduler: timerScheduler), 1) + + let block2 = try makeBlock(fromFile: "states_visibility", context: context) + let view2 = block2.reuse( + view, + observer: nil, + overscrollDelegate: nil, + renderingDelegate: nil, + superview: nil + ) + XCTAssertEqual(getViewVisibilityCallCount(view: view2, rect: rect, timerScheduler: timerScheduler), 0) + } +} + +private func makeBlock( + fromFile filename: String, + context: DivBlockModelingContext = .default +) throws -> Block { + try DivStateTemplate.make( + fromFile: filename, + subdirectory: "div-state", + context: context + ) +} diff --git a/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift b/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift new file mode 100644 index 000000000..a9cb00fb4 --- /dev/null +++ b/client/ios/DivKitTests/Extensions/XCTestCase+ViewVisibilityCallCount.swift @@ -0,0 +1,28 @@ +import LayoutKit +import XCTest + +extension XCTestCase { + func getViewVisibilityCallCount( + view: BlockView, + rect: CGRect, + timerScheduler: TestTimerScheduler + ) -> Int { + let performer = UIActionEventPerformerMock() + + performer.addSubview(view) + view.frame = rect + view.layoutIfNeeded() + view.onVisibleBoundsChanged(from: rect, to: rect) + timerScheduler.timers.forEach { guard $0.isValid else { return }; $0.fire() } + + return performer.callCount + } +} + +private class UIActionEventPerformerMock: UIView, UIActionEventPerforming { + var callCount: Int = 0 + + func perform(uiActionEvent _: UIActionEvent, from _: AnyObject) { + callCount += 1 + } +} diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift index f628293d2..228984d28 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/Block+Decorations.swift @@ -27,6 +27,8 @@ extension Block { longTapActions: LongTapActions? = nil, analyticsURL: URL? = nil, visibilityActions: [VisibilityAction]? = nil, + scheduler: Scheduling? = nil, + lastVisibleBounds: Property? = nil, tooltips: [BlockTooltip]? = nil, forceWrapping: Bool, accessibilityElement: AccessibilityElement? = nil @@ -92,6 +94,8 @@ extension Block { childAlpha: alpha.map { $0 * block.childAlpha }, blurEffect: blurEffect ?? block.blurEffect, visibilityActions: visibilityActions ?? block.visibilityActions, + scheduler: scheduler ?? block.scheduler, + lastVisibleBounds: lastVisibleBounds ?? block.lastVisibleBounds, tooltips: [tooltips, block.tooltips].compactMap { $0 }.flatMap { $0 }, accessibilityElement: accessibilityElement ) @@ -116,6 +120,8 @@ extension Block { childAlpha: alpha ?? DecoratingBlock.defaultChildAlpha, blurEffect: blurEffect, visibilityActions: visibilityActions ?? [], + scheduler: scheduler, + lastVisibleBounds: lastVisibleBounds, tooltips: tooltips ?? [], accessibilityElement: accessibilityElement ) @@ -143,6 +149,8 @@ extension Block { analyticsURL: URL? = nil, shadow: BlockShadow? = nil, visibilityActions: [VisibilityAction]? = nil, + lastVisibleBounds: Property? = nil, + scheduler: Scheduling? = nil, tooltips: [BlockTooltip]? = nil, forceWrapping: Bool = false, accessibilityElement: AccessibilityElement? = nil @@ -160,6 +168,8 @@ extension Block { longTapActions: longTapActions, analyticsURL: analyticsURL, visibilityActions: visibilityActions, + scheduler: scheduler, + lastVisibleBounds: lastVisibleBounds, tooltips: tooltips, forceWrapping: forceWrapping, accessibilityElement: accessibilityElement diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift index b245f6ab8..75ee695a5 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Decorations/DecoratingBlock.swift @@ -25,6 +25,8 @@ final class DecoratingBlock: WrapperBlock { let blurEffect: BlurEffect? let paddings: EdgeInsets let visibilityActions: [VisibilityAction] + let scheduler: Scheduling? + let lastVisibleBounds: Property? let tooltips: [BlockTooltip] let accessibilityElement: AccessibilityElement? @@ -43,6 +45,8 @@ final class DecoratingBlock: WrapperBlock { blurEffect: BlurEffect? = nil, paddings: EdgeInsets = .zero, visibilityActions: [VisibilityAction] = [], + scheduler: Scheduling? = nil, + lastVisibleBounds: Property? = nil, tooltips: [BlockTooltip] = [], accessibilityElement: AccessibilityElement? = nil ) { @@ -60,6 +64,8 @@ final class DecoratingBlock: WrapperBlock { self.blurEffect = blurEffect self.paddings = paddings self.visibilityActions = visibilityActions + self.scheduler = scheduler + self.lastVisibleBounds = lastVisibleBounds self.tooltips = tooltips self.accessibilityElement = accessibilityElement } @@ -144,6 +150,8 @@ extension DecoratingBlock { blurEffect: BlurEffect? = nil, paddings: EdgeInsets? = nil, visibilityActions: [VisibilityAction]? = nil, + scheduler: Scheduling? = nil, + lastVisibleBounds: Property? = nil, tooltips: [BlockTooltip]? = nil, accessibilityElement: AccessibilityElement? = nil ) -> DecoratingBlock { @@ -162,6 +170,8 @@ extension DecoratingBlock { blurEffect: blurEffect ?? self.blurEffect, paddings: paddings ?? self.paddings, visibilityActions: visibilityActions ?? self.visibilityActions, + scheduler: scheduler ?? self.scheduler, + lastVisibleBounds: lastVisibleBounds ?? self.lastVisibleBounds, tooltips: tooltips ?? self.tooltips, accessibilityElement: accessibilityElement ?? self.accessibilityElement ) diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/StateBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/StateBlock.swift index 478222155..01a1fe1c8 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/StateBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/StateBlock.swift @@ -2,16 +2,13 @@ import CommonCorePublic public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl { public let child: Block - public let stateId: String public let ids: Set public init( child: Block, - stateId: String, ids: Set ) { self.child = child - self.stateId = stateId self.ids = ids } @@ -23,14 +20,13 @@ public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl { } public func makeCopy(wrapping child: Block) -> StateBlock { - StateBlock(child: child, stateId: stateId, ids: ids) + StateBlock(child: child, ids: ids) } } extension StateBlock: Equatable { public static func ==(lhs: StateBlock, rhs: StateBlock) -> Bool { lhs.child == rhs.child - && lhs.stateId == rhs.stateId && lhs.ids == rhs.ids } } @@ -41,9 +37,8 @@ extension StateBlock: CustomDebugStringConvertible { extension Block { public func addingStateBlock( - stateId: String, ids: Set ) -> Block { - StateBlock(child: self, stateId: stateId, ids: ids) + StateBlock(child: self, ids: ids) } } diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift index 8c5f2a40a..ddc31bea6 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift @@ -38,7 +38,9 @@ extension DecoratingBlock { blurEffect: blurEffect, paddings: paddings, source: Variable { [weak self] in self }, + scheduler: scheduler, visibilityActions: visibilityActions, + lastVisibleBounds: lastVisibleBounds, tooltips: tooltips, accessibility: accessibilityElement ) @@ -101,7 +103,9 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT let blurEffect: BlurEffect? let paddings: EdgeInsets let source: Variable + let scheduler: Scheduling? let visibilityActions: [VisibilityAction] + let lastVisibleBounds: Property? let tooltips: [BlockTooltip] let accessibility: AccessibilityElement? @@ -289,6 +293,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT guard model != self.model || observer !== self.observer else { return } + let oldModel = self.model if self.model?.tooltips.isEmpty ?? true, !model.tooltips.isEmpty { renderingDelegate?.tooltipAnchorViewAdded(anchorView: self) @@ -320,24 +325,28 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT model.actions? .forEach { applyAccessibility($0.accessibilityElement) } - visibilityActionPerformers = model.visibilityActions.isEmpty - ? nil - : VisibilityActionPerformers( - visibilityCheckParams: model.visibilityActions.map { visibilityAction in - VisibilityCheckParam( - requiredDuration: visibilityAction.requiredDuration, - targetPercentage: visibilityAction.targetPercentage, - limiter: visibilityAction.limiter, - action: { [unowned self] in - UIActionEvent( - uiAction: visibilityAction.uiAction, - originalSender: self - ).sendFrom(self) - }, - type: visibilityAction.actionType - ) - } - ) + if oldModel?.visibilityActions != model.visibilityActions { + visibilityActionPerformers = model.visibilityActions.isEmpty + ? nil + : VisibilityActionPerformers( + visibilityCheckParams: model.visibilityActions.map { visibilityAction in + VisibilityCheckParam( + requiredDuration: visibilityAction.requiredDuration, + targetPercentage: visibilityAction.targetPercentage, + limiter: visibilityAction.limiter, + action: { [unowned self] in + UIActionEvent( + uiAction: visibilityAction.uiAction, + originalSender: self + ).sendFrom(self) + }, + type: visibilityAction.actionType + ) + }, + lastVisibleBounds: model.lastVisibleBounds ?? Property(initialValue: .zero), + scheduling: model.scheduler ?? TimerScheduler() + ) + } tapRecognizer.isEnabled = model.shouldHandleTap doubleTapRecognizer.isEnabled = model.shouldHandleDoubleTap diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift index ba9095f4a..8fa671614 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift @@ -19,7 +19,6 @@ extension StateBlock { ) { (view as! StateBlockView).configure( child: child, - stateId: stateId, ids: Set(ids.map { BlockViewID(rawValue: $0) }), observer: observer, overscrollDelegate: overscrollDelegate, @@ -109,7 +108,6 @@ private final class StateBlockView: BlockView { private var subviewStorage = SubviewStorage(wrappedRenderingDelegate: nil, ids: []) private var childView: BlockView? private var stateId: String? - private var isStateChanged = false var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } @@ -124,17 +122,11 @@ private final class StateBlockView: BlockView { func configure( child: Block, - stateId: String, ids: Set, observer: ElementStateObserver?, overscrollDelegate: ScrollDelegate?, renderingDelegate: RenderingDelegate? ) { - if (self.stateId != stateId) { - isStateChanged = true - self.stateId = stateId - } - // remove views with unfinished animations subviews.forEach { if $0 !== childView { @@ -188,15 +180,6 @@ private final class StateBlockView: BlockView { } extension StateBlockView: VisibleBoundsTrackingContainer { - func onVisibleBoundsChanged(from: CGRect, to: CGRect) { - if isStateChanged { - isStateChanged = false - passVisibleBoundsChanged(from: .zero, to: to) - } else { - passVisibleBoundsChanged(from: from, to: to) - } - } - var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { childView.asArray() } diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift index a48851d2e..61d28a5ba 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformer.swift @@ -22,7 +22,7 @@ final class VisibilityActionPerformer { limiter: ActionLimiter, action: @escaping Action, type: VisibilityActionType, - timerScheduler: Scheduling = TimerScheduler() + timerScheduler: Scheduling ) { self.requiredDuration = requiredDuration self.targetPercentage = targetVisibilityPercentage diff --git a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformers.swift b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformers.swift index 2da247c67..2492a6f8e 100644 --- a/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformers.swift +++ b/client/ios/LayoutKit/LayoutKit/ViewModels/VisibilityActionPerformer/VisibilityActionPerformers.swift @@ -1,15 +1,29 @@ import CoreGraphics import Foundation +import BasePublic + public final class VisibilityActionPerformers { private let actionPerformers: [VisibilityActionPerformer] + private let lastVisibleBounds: Property - init(visibilityCheckParams: [VisibilityCheckParam]) { - actionPerformers = visibilityCheckParams.map(VisibilityActionPerformer.init) + init( + visibilityCheckParams: [VisibilityCheckParam], + lastVisibleBounds: Property, + scheduling: Scheduling + ) { + self.lastVisibleBounds = lastVisibleBounds + actionPerformers = visibilityCheckParams.map { + VisibilityActionPerformer(visibilityCheckParam: $0, scheduling: scheduling) + } } - func onVisibleBoundsChanged(from: CGRect, to: CGRect, bounds: CGRect) { - let visibleAreaPercentageBefore = bounds.isEmpty ? 0 : Int(from.area * 100 / bounds.area) + func onVisibleBoundsChanged(from _: CGRect, to: CGRect, bounds: CGRect) { + let beforeVisibleBounds = lastVisibleBounds.value + lastVisibleBounds.value = to + + let visibleAreaPercentageBefore = bounds + .isEmpty ? 0 : Int(beforeVisibleBounds.area * 100 / bounds.area) let visibleAreaPercentageAfter = bounds.isEmpty ? 0 : Int(to.area * 100 / bounds.area) for performer in actionPerformers { @@ -22,13 +36,14 @@ public final class VisibilityActionPerformers { } extension VisibilityActionPerformer { - fileprivate convenience init(visibilityCheckParam: VisibilityCheckParam) { + fileprivate convenience init(visibilityCheckParam: VisibilityCheckParam, scheduling: Scheduling) { self.init( requiredDuration: visibilityCheckParam.requiredDuration, targetVisibilityPercentage: visibilityCheckParam.targetPercentage, limiter: visibilityCheckParam.limiter, action: visibilityCheckParam.action, - type: visibilityCheckParam.type + type: visibilityCheckParam.type, + timerScheduler: scheduling ) } } diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/chromeMobile/div-text-visibility-actions-gone.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/chromeMobile/div-text-visibility-actions-gone.png new file mode 100644 index 000000000..22c71d369 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/chromeMobile/div-text-visibility-actions-gone.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/firefoxMobile/div-text-visibility-actions-gone.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/firefoxMobile/div-text-visibility-actions-gone.png new file mode 100644 index 000000000..25de45cff Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-gone/firefoxMobile/div-text-visibility-actions-gone.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/chromeMobile/div-text-visibility-actions-invisible.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/chromeMobile/div-text-visibility-actions-invisible.png new file mode 100644 index 000000000..22c71d369 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/chromeMobile/div-text-visibility-actions-invisible.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/firefoxMobile/div-text-visibility-actions-invisible.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/firefoxMobile/div-text-visibility-actions-invisible.png new file mode 100644 index 000000000..25de45cff Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-invisible/firefoxMobile/div-text-visibility-actions-invisible.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/chromeMobile/div-text-visibility-actions-visible.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/chromeMobile/div-text-visibility-actions-visible.png new file mode 100644 index 000000000..8f725a2e4 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/chromeMobile/div-text-visibility-actions-visible.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/firefoxMobile/div-text-visibility-actions-visible.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/firefoxMobile/div-text-visibility-actions-visible.png new file mode 100644 index 000000000..9052dae50 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-base/div-text-visibility-actions-visible/firefoxMobile/div-text-visibility-actions-visible.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/chromeMobile/root_states_visibility.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/chromeMobile/root_states_visibility.png new file mode 100644 index 000000000..c69f834bd Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/chromeMobile/root_states_visibility.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/firefoxMobile/root_states_visibility.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/firefoxMobile/root_states_visibility.png new file mode 100644 index 000000000..c95b102e8 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-data/root_states_visibility/firefoxMobile/root_states_visibility.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/chromeMobile/states_visibility.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/chromeMobile/states_visibility.png new file mode 100644 index 000000000..fe2fae05f Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/chromeMobile/states_visibility.png differ diff --git a/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/firefoxMobile/states_visibility.png b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/firefoxMobile/states_visibility.png new file mode 100644 index 000000000..564e2a687 Binary files /dev/null and b/client/web/divkit/tests/hermione/screens/crossplatform/unit/div-state/states_visibility/firefoxMobile/states_visibility.png differ diff --git a/test_data/unit_test_data/div-base/div-text-visibility-actions-gone.json b/test_data/unit_test_data/div-base/div-text-visibility-actions-gone.json new file mode 100644 index 000000000..c25134314 --- /dev/null +++ b/test_data/unit_test_data/div-base/div-text-visibility-actions-gone.json @@ -0,0 +1,11 @@ +{ + "type": "text", + "text": "text", + "visibility": "gone", + "visibility_actions": [ + { + "log_id": "some", + "log_limit": 0 + } + ] +} diff --git a/test_data/unit_test_data/div-base/div-text-visibility-actions-invisible.json b/test_data/unit_test_data/div-base/div-text-visibility-actions-invisible.json new file mode 100644 index 000000000..17e224f1d --- /dev/null +++ b/test_data/unit_test_data/div-base/div-text-visibility-actions-invisible.json @@ -0,0 +1,12 @@ +{ + "type": "text", + "text": "text", + "visibility": "invisible", + "visibility_actions": [ + { + "log_id": "some", + "log_limit": 0 + } + ] +} + diff --git a/test_data/unit_test_data/div-base/div-text-visibility-actions-visible.json b/test_data/unit_test_data/div-base/div-text-visibility-actions-visible.json new file mode 100644 index 000000000..c20133bbe --- /dev/null +++ b/test_data/unit_test_data/div-base/div-text-visibility-actions-visible.json @@ -0,0 +1,12 @@ +{ + "type": "text", + "text": "text", + "visibility": "visibile", + "visibility_actions": [ + { + "log_id": "some", + "log_limit": 0, + "visibility_duration": 0 + } + ] +} diff --git a/test_data/unit_test_data/div-data/root_states_visibility.json b/test_data/unit_test_data/div-data/root_states_visibility.json new file mode 100644 index 000000000..2ac3b18d1 --- /dev/null +++ b/test_data/unit_test_data/div-data/root_states_visibility.json @@ -0,0 +1,28 @@ +{ + "log_id": "sample_card", + "states": [ + { + "state_id": 0, + "div": { + "type": "text", + "text": "state zero", + "visibility_action": { + "log_id": "zero state visible", + "log_limit": 0 + } + } + }, + { + "state_id": 1, + "div": { + "type": "text", + "text": "state first", + "visibility_action": { + "log_id": "first state visible", + "log_limit": 0 + } + } + } + ] +} + diff --git a/test_data/unit_test_data/div-state/states_visibility.json b/test_data/unit_test_data/div-state/states_visibility.json new file mode 100644 index 000000000..610c96f60 --- /dev/null +++ b/test_data/unit_test_data/div-state/states_visibility.json @@ -0,0 +1,28 @@ +{ + "type": "state", + "id": "mystate", + "states": [ + { + "state_id": "first", + "div": { + "type": "text", + "text": "state first", + "visibility_action": { + "log_id": "first state visible", + "log_limit": 0 + } + } + }, + { + "state_id": "second", + "div": { + "type": "text", + "text": "seconds first", + "visibility_action": { + "log_id": "second state visible", + "log_limit": 0 + } + } + } + ] +}