diff --git a/.mapping.json b/.mapping.json index 924f39c8a..9ac7b0b11 100644 --- a/.mapping.json +++ b/.mapping.json @@ -9687,6 +9687,7 @@ "client/ios/DivKitSnapshotTests/YSFontProvider.swift":"divkit/public/client/ios/DivKitSnapshotTests/YSFontProvider.swift", "client/ios/DivKitTests/Actions/DivActionHandlerTests.swift":"divkit/public/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift", "client/ios/DivKitTests/Actions/DivActionIntentTests.swift":"divkit/public/client/ios/DivKitTests/Actions/DivActionIntentTests.swift", + "client/ios/DivKitTests/Actions/DivActionURLHandlerTests.swift":"divkit/public/client/ios/DivKitTests/Actions/DivActionURLHandlerTests.swift", "client/ios/DivKitTests/DivBlockModelingContextTests.swift":"divkit/public/client/ios/DivKitTests/DivBlockModelingContextTests.swift", "client/ios/DivKitTests/DivBlockStateStorageTests.swift":"divkit/public/client/ios/DivKitTests/DivBlockStateStorageTests.swift", "client/ios/DivKitTests/DivDeserializationErrorsTests.swift":"divkit/public/client/ios/DivKitTests/DivDeserializationErrorsTests.swift", diff --git a/client/ios/DivKit.xcodeproj/project.pbxproj b/client/ios/DivKit.xcodeproj/project.pbxproj index d08fe1565..6a16039ad 100644 --- a/client/ios/DivKit.xcodeproj/project.pbxproj +++ b/client/ios/DivKit.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 0A9A9AA028CB1A9200D1D810 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 0A9A9A9F28CB1A9200D1D810 /* Lottie */; }; 0AB4193E28BCF61D00FDAAD5 /* LottieAnimationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB4193D28BCF61D00FDAAD5 /* LottieAnimationFactory.swift */; }; 431276EE2A72B8F2000CC821 /* DivView+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431276ED2A72B8F2000CC821 /* DivView+SwiftUI.swift */; }; + 431CF8582B7BC47E005A2E61 /* DivActionURLHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431CF8572B7BC47E005A2E61 /* DivActionURLHandlerTests.swift */; }; 431DC9C22961EF84007FA268 /* ContainerBlockLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431DC9C12961EF84007FA268 /* ContainerBlockLayoutTests.swift */; }; 431DC9C429620AA1007FA268 /* BlockTestsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431DC9C329620AA1007FA268 /* BlockTestsModels.swift */; }; 435653DB29742C9300224B9D /* BlockWithFixedWrapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435653DA29742C9300224B9D /* BlockWithFixedWrapContent.swift */; }; @@ -275,6 +276,7 @@ /* Begin PBXFileReference section */ 0AB4193D28BCF61D00FDAAD5 /* LottieAnimationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationFactory.swift; sourceTree = ""; }; 431276ED2A72B8F2000CC821 /* DivView+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DivView+SwiftUI.swift"; sourceTree = ""; }; + 431CF8572B7BC47E005A2E61 /* DivActionURLHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DivActionURLHandlerTests.swift; sourceTree = ""; }; 431DC9C12961EF84007FA268 /* ContainerBlockLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerBlockLayoutTests.swift; sourceTree = ""; }; 431DC9C329620AA1007FA268 /* BlockTestsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockTestsModels.swift; sourceTree = ""; }; 435653DA29742C9300224B9D /* BlockWithFixedWrapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockWithFixedWrapContent.swift; sourceTree = ""; }; @@ -744,6 +746,7 @@ children = ( 8C7B1B312865C01C0036EF4C /* DivActionIntentTests.swift */, 8C40BA012ABB3B5C008405AD /* DivActionHandlerTests.swift */, + 431CF8572B7BC47E005A2E61 /* DivActionURLHandlerTests.swift */, ); path = Actions; sourceTree = ""; @@ -1638,6 +1641,7 @@ 8C7B1CA92865C01E0036EF4C /* DivStateManagerTests.swift in Sources */, 8C7B1BFD2865C01C0036EF4C /* DivSeparatorExtensionsTests.swift in Sources */, 8C23F41229DDB3810069F3F7 /* EntityTemplate.swift in Sources */, + 431CF8582B7BC47E005A2E61 /* DivActionURLHandlerTests.swift in Sources */, 8C23F43629DDB3810069F3F7 /* EntityWithStringEnumPropertyWithDefaultValueTemplate.swift in Sources */, 8C23F41C29DDB3810069F3F7 /* EntityWithArrayWithTransformTemplate.swift in Sources */, 8CD1521028CF217B0061128B /* ExpressionResolverTests.swift in Sources */, diff --git a/client/ios/DivKit/Actions/DivActionURLHandler.swift b/client/ios/DivKit/Actions/DivActionURLHandler.swift index 1f458f52f..e9b5e03cd 100644 --- a/client/ios/DivKit/Actions/DivActionURLHandler.swift +++ b/client/ios/DivKit/Actions/DivActionURLHandler.swift @@ -214,6 +214,9 @@ public final class DivActionURLHandler { count: Int, overflow: OverflowMode ) -> Int { + guard count != 0 else { + return current + } switch overflow { case .ring: return (current + 1) % count @@ -270,9 +273,12 @@ public final class DivActionURLHandler { count: Int, overflow: OverflowMode ) -> Int { + guard count != 0 else { + return current + } switch overflow { case .ring: - return (count + (current - 1)) % count + return (current + count - 1) % count case .clamp: return max(0, current - 1) } @@ -284,18 +290,22 @@ public final class DivActionURLHandler { index: Int, itemsCount: Int ) { + let clampedIndex = clamp(index, min: 0, max: max(0, itemsCount - 1)) + guard clampedIndex == index else { + return + } blockStateStorage.setState( id: id, cardId: cardId, state: GalleryViewState( - contentPageIndex: CGFloat(max(0, index)), + contentPageIndex: CGFloat(clampedIndex), itemsCount: itemsCount ) ) } private func setPagerCurrentItem(id: String, cardId: DivCardID, index: Int, numberOfPages: Int) { - let clampedIndex = clamp(index, min: 0, max: numberOfPages - 1) + let clampedIndex = clamp(index, min: 0, max: max(0, numberOfPages - 1)) guard clampedIndex == index else { return } @@ -311,11 +321,15 @@ public final class DivActionURLHandler { } private func setTabsCurrentItem(id: String, cardId: DivCardID, index: Int, countOfPages: Int) { + let clampedIndex = clamp(index, min: 0, max: max(0, countOfPages - 1)) + guard clampedIndex == index else { + return + } blockStateStorage.setState( id: id, cardId: cardId, state: TabViewState( - selectedPageIndex: CGFloat(max(0, index)), + selectedPageIndex: CGFloat(clampedIndex), countOfPages: countOfPages ) ) diff --git a/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift b/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift index 49a6c4b93..9c321677b 100644 --- a/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift +++ b/client/ios/DivKitTests/Actions/DivActionHandlerTests.swift @@ -148,7 +148,7 @@ private final class MockActionLogger: DivActionLogger { } } -private final class MockPatchProvider: DivPatchProvider { +final class MockPatchProvider: DivPatchProvider { func getPatch(url _: URL, completion _: @escaping DivPatchProviderCompletion) {} func cancelRequests() {} diff --git a/client/ios/DivKitTests/Actions/DivActionURLHandlerTests.swift b/client/ios/DivKitTests/Actions/DivActionURLHandlerTests.swift new file mode 100644 index 000000000..96b3d3116 --- /dev/null +++ b/client/ios/DivKitTests/Actions/DivActionURLHandlerTests.swift @@ -0,0 +1,171 @@ +@testable import DivKit +@testable import LayoutKit + +import XCTest + +final class DivActionURLHandlerTests: XCTestCase { + private var actionHandler: DivActionURLHandler! + private let blockStateStorage = DivBlockStateStorage() + + override func setUp() { + actionHandler = DivActionURLHandler( + stateUpdater: DefaultDivStateManagement(), + blockStateStorage: blockStateStorage, + patchProvider: MockPatchProvider(), + variableUpdater: DivVariablesStorage(), + updateCard: { _ in }, + showTooltip: nil, + tooltipActionPerformer: nil, + persistentValuesStorage: DivPersistentValuesStorage() + ) + } + + func test_setNextItemAction() { + setItemTestCases( + beforeStates: SetItemAction.firstElementState, + afterState: SetItemAction.secondElementState, + mode: .next(.clamp) + ) + } + + func test_setNextItemActionWithRingOverflow() { + setItemTestCases( + beforeStates: SetItemAction.lastElementsStates, + afterState: SetItemAction.firstElementState, + mode: .next(.ring) + ) + } + + func test_setNextItemActionWithEmptyState() { + setItemTestCases( + beforeStates: SetItemAction.emptyStates, + afterState: SetItemAction.emptyStates, + mode: .next(.clamp) + ) + } + + func test_setPreviousItemAction() { + setItemTestCases( + beforeStates: SetItemAction.secondElementState, + afterState: SetItemAction.firstElementState, + mode: .previous(.clamp) + ) + } + + func test_setPreviousItemActionWithRingOverflow() { + setItemTestCases( + beforeStates: SetItemAction.firstElementState, + afterState: SetItemAction.lastElementsStates, + mode: .previous(.ring) + ) + } + + func test_setPreviousItemActionWithEmptyState() { + setItemTestCases( + beforeStates: SetItemAction.emptyStates, + afterState: SetItemAction.emptyStates, + mode: .previous(.clamp) + ) + } + + func test_setCurrentItemAction() { + setItemTestCases( + beforeStates: SetItemAction.firstElementState, + afterState: SetItemAction.firstElementState, + mode: .current(0) + ) + setItemTestCases( + beforeStates: SetItemAction.firstElementState, + afterState: SetItemAction.secondElementState, + mode: .current(1) + ) + } + + func test_setCurrentItemActionWithOverflow() { + setItemTestCases( + beforeStates: SetItemAction.firstElementState, + afterState: SetItemAction.firstElementState, + mode: .current(50) + ) + } + + private func setItemTestCases( + beforeStates: [ElementState], + afterState: [ElementState], + mode: SetItemAction.Mode + ) { + zip(beforeStates, afterState).forEach { + switch $0 { + case let (before as GalleryViewState, after as GalleryViewState): + setItemTestCase(beforeState: before, afterState: after, mode: mode) + case let (before as PagerViewState, after as PagerViewState): + setItemTestCase(beforeState: before, afterState: after, mode: mode) + case let (before as TabViewState, after as TabViewState): + setItemTestCase(beforeState: before, afterState: after, mode: mode) + case (_, _): + assertionFailure("Unsupported element state") + } + } + } + + private func setItemTestCase( + beforeState: State, + afterState: State, + mode: SetItemAction.Mode + ) { + blockStateStorage.setState(id: elementID, cardId: cardID, state: beforeState) + let _ = actionHandler.handleURL(SetItemAction.makeURL(mode: mode), cardId: cardID) + XCTAssertEqual(blockStateStorage.getState(elementID, cardId: cardID), afterState) + } +} + +private enum SetItemAction { + enum Mode { + enum Overflow: String { + case clamp + case ring + } + + case next(Overflow) + case previous(Overflow) + case current(Int) + } + + static let firstElementState: [ElementState] = [ + PagerViewState(numberOfPages: 10, currentPage: 0), + GalleryViewState(contentPageIndex: 0, itemsCount: 10), + TabViewState(selectedPageIndex: 0, countOfPages: 10), + ] + + static let secondElementState: [ElementState] = [ + PagerViewState(numberOfPages: 10, currentPage: 1), + GalleryViewState(contentPageIndex: 1, itemsCount: 10), + TabViewState(selectedPageIndex: 1, countOfPages: 10), + ] + + static let lastElementsStates: [ElementState] = [ + PagerViewState(numberOfPages: 10, currentPage: 9), + GalleryViewState(contentPageIndex: 9, itemsCount: 10), + TabViewState(selectedPageIndex: 9, countOfPages: 10), + ] + + static let emptyStates: [ElementState] = [ + PagerViewState(numberOfPages: 0, currentPage: 0), + GalleryViewState(contentPageIndex: 0, itemsCount: 0), + TabViewState(selectedPageIndex: 0, countOfPages: 0), + ] + + static func makeURL(mode: Mode) -> URL { + switch mode { + case let .next(overflow): + url("div-action://set_next_item?id=\(elementID)&overflow=\(overflow)") + case let .previous(overflow): + url("div-action://set_previous_item?id=\(elementID)&overflow=\(overflow)") + case let .current(item): + url("div-action://set_current_item?id=\(elementID)&item=\(item)") + } + } +} + +private let cardID: DivCardID = "cardID" +private let elementID: String = "element"