@testable @_spi(Internal) import DivKit import DivKitTestsSupport import LayoutKit import VGSL import XCTest final class DivDataPatchExtensionsTests: XCTestCase { private var callbacksCount = 0 private lazy var divPatchCallbacks = Callbacks { _ in self.callbacksCount += 1 } private var flags: DivFlagsInfo = .default private var isUpdateCardCalled = false private lazy var handledUrls = [URL]() private lazy var actionHandler = DivActionHandler( urlHandler: DivUrlHandlerDelegate { [unowned self] url, _ in handledUrls.append(url) } ) private lazy var context = DivBlockModelingContext( actionHandler: actionHandler ) func test_WhenNoSuitableChanges_DoesNothing() throws { let originalData = divData( divContainer( items: [ divText(id: "some_div", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", divSeparator()) ) XCTAssertEqual(patchedData, originalData) } func test_WhenReplacingRootDiv_ReplacesDiv() throws { let originalData = divData( divText(id: "div_to_replace", text: "Old text") ) let patchedData = originalData.applyPatch( ("div_to_replace", newDivText1) ) let expectedData = divData( newDivText1 ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingDivWithDifferentType_ReplacesDiv() throws { let oridinalData = divData( divText(id: "div_to_replace", text: "Old text") ) let patchedData = oridinalData.applyPatch( ("div_to_replace", divSeparator()) ) let expectedData = divData( divSeparator() ) XCTAssertEqual(patchedData, expectedData) } func test_WhenDeletingRootDiv_DoesNothing() throws { let originalData = divData( divText(id: "div_to_delete", text: "Old text") ) let patchedData = originalData.applyPatch( ("div_to_delete", nil) ) XCTAssertEqual(patchedData, originalData) } func test_WhenReplacingRootDivWithMultipleDivs_DoesNothing() throws { let originalData = divData( divText(id: "div_to_replace", text: "Old text") ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]) ) XCTAssertEqual(patchedData, originalData) } func test_WhenReplacingItemInContainer_ReplacesDiv() throws { let originalData = divData( divContainer( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", newDivText1) ) let expectedData = divData( divContainer( items: [ divSeparator(), newDivText1, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingItemsInContainer_ReplacesDivs() throws { let originalData = divData( divContainer( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]) ) let expectedData = divData( divContainer( items: [ divSeparator(), newDivText1, newDivText2, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenDeletingItemInContainer_RemovesDiv() throws { let originalData = divData( divContainer( items: [ divSeparator(), divText(id: "div_to_delete", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_delete", nil) ) let expectedData = divData( divContainer( items: [ divSeparator(), divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenHaveMultipleChanges_AppliesAll() throws { let originalData = divData( divContainer( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divText(id: "div_to_delete", text: "Text to delete"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]), ("div_to_delete", nil) ) let expectedData = divData( divContainer( items: [ divSeparator(), newDivText1, newDivText2, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingItemsInGallery_ReplacesDivs() throws { let originalData = divData( divGallery( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]) ) let expectedData = divData( divGallery( items: [ divSeparator(), newDivText1, newDivText2, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenDeletingItemInGallery_RemovesDiv() throws { let originalData = divData( divGallery( items: [ divSeparator(), divText(id: "div_to_delete", text: "Old text"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_delete", nil) ) let expectedData = divData( divGallery( items: [ divSeparator(), divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingAndDeletingItemsInGrid_AppliesAll() throws { let originalData = divData( divGrid( columnCount: 1, items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divSeparator(id: "div_to_delete"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]), ("div_to_delete", nil) ) let expectedData = divData( divGrid( columnCount: 1, items: [ divSeparator(), newDivText1, newDivText2, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingAndDeletingItemsInPager_AppliesAll() throws { let originalData = divData( makePager( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divSeparator(id: "div_to_delete"), divSeparator(), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]), ("div_to_delete", nil) ) let expectedData = divData( makePager( items: [ divSeparator(), newDivText1, newDivText2, divSeparator(), ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingStateDiv_ReplacesDiv() throws { let originalData = divData( makeState( states: [ divText(id: "div_to_replace", text: "Old text"), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", newDivText1) ) let expectedData = divData( makeState( states: [ newDivText1, ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenDeletingStateDiv_KeepsEmptyState() throws { let originalData = divData( makeState( states: [ divText(id: "div_to_delete", text: "Old text"), ] ) ) let patchedData = originalData.applyPatch( ("div_to_delete", nil) ) let expectedData = divData( makeState( states: [nil] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenReplacingStateDivWithMultipleDivs_DoesNothing() throws { let originalData = divData( makeState( states: [ divText(id: "div_to_replace", text: "Old text"), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", [newDivText1, newDivText2]) ) XCTAssertEqual(patchedData, originalData) } func test_WhenReplacingTabsDiv_ReplacesDiv() throws { let originalData = divData( makeTabs( items: [ divText(id: "div_to_replace", text: "Old text"), ] ) ) let patchedData = originalData.applyPatch( ("div_to_replace", newDivText1) ) let expectedData = divData( makeTabs( items: [ newDivText1, ] ) ) XCTAssertEqual(patchedData, expectedData) } func test_WhenDeletingTabsDiv_DoesNothing() throws { let originalData = divData( makeTabs( items: [ divText(id: "div_to_delete", text: "Old text"), ] ) ) let patchedData = originalData.applyPatch( ("div_to_delete", nil) ) XCTAssertEqual(patchedData, originalData) } func test_WhenReplacesSuitableDiv_CallElementChangedCallback() throws { let originalData = divData( divContainer( items: [ divText(id: "some_div", text: "Old text"), divSeparator(), ] ) ) _ = originalData.applyPatch( ("some_div", divSeparator()), callbacks: divPatchCallbacks ) XCTAssertEqual(callbacksCount, 1) } func test_WhenNoSuitableChanges_DoesNotCallElementChangedCallback() throws { let originalData = divData( divContainer( items: [ divText(id: "some_div", text: "Old text"), divSeparator(), ] ) ) _ = originalData.applyPatch( ("div_to_replace", divSeparator()), callbacks: divPatchCallbacks ) XCTAssertEqual(callbacksCount, 0) } func test_WhenReplacingDivInMultipleItemsContainer_CallOnlyElementChangedCallbacks() throws { let originalData = divData( divContainer( items: [ divSeparator(), divText(id: "div_to_replace", text: "Old text"), divText(id: "some_div", text: "Old text 2"), divSeparator(), ] ) ) _ = originalData.applyPatch( ("div_to_replace", [newDivText1]), ("div_to_replace_2", [newDivText1]), callbacks: divPatchCallbacks ) XCTAssertEqual(callbacksCount, 1) } func test_ApplyPatchWithActions_DefaultMode_AppliedPartialDataAndExecuteAppliedActions() throws { let originalData = divData( divContainer( items: [ divText(id: "div_to_replace", text: "Old text"), divSeparator(), ] ) ) let expectedData = divData( divContainer( items: [ newDivText1, divSeparator(), ] ) ) let patch = DivPatch( changes: [ DivPatch.Change(id: "div_to_replace", items: [newDivText1]), DivPatch.Change(id: "error_div_id", items: [newDivText2]), ], mode: .value(.partial), onAppliedActions: [divAction(logId: "applied", url: "action://applied")], onFailedActions: [divAction(logId: "failed", url: "action://failed")] ) let result = originalData.applyPatchWithActions( patch, context: context ) XCTAssertEqual(result, expectedData) XCTAssertEqual(handledUrls, [URL(string: "action://applied")!]) } func test_ApplyPatchWithActions_TransactionalMode_AllChangesApplied_ExecutesOnAppliedActions( ) throws { let originalData = divData( divContainer( items: [ divText(id: "div_to_replace", text: "Old text"), divSeparator(), divText(id: "div_to_replace_2", text: "Old text 2"), ] ) ) let expectedData = divData( divContainer( items: [ newDivText1, divSeparator(), newDivText2, ] ) ) let patch = DivPatch( changes: [ DivPatch.Change(id: "div_to_replace", items: [newDivText1]), DivPatch.Change(id: "div_to_replace_2", items: [newDivText2]), ], mode: .value(.transactional), onAppliedActions: [divAction(logId: "applied", url: "action://applied")], onFailedActions: [divAction(logId: "failed", url: "action://failed")] ) let result = originalData.applyPatchWithActions( patch, context: context ) XCTAssertEqual(result, expectedData) XCTAssertEqual(handledUrls, [URL(string: "action://applied")!]) } func test_ApplyPatchWithActions_TransactionalMode_NotAllChangesApplied_ExecutesOnFailedActions( ) throws { let originalData = divData( divContainer( items: [ divText(id: "div_to_replace", text: "Old text"), divSeparator(), ] ) ) let patch = DivPatch( changes: [ DivPatch.Change(id: "div_to_replace", items: [newDivText1]), DivPatch.Change(id: "non_existent_div", items: [newDivText2]), ], mode: .value(.transactional), onAppliedActions: [divAction(logId: "applied", url: "action://applied")], onFailedActions: [divAction(logId: "failed", url: "action://failed")] ) let result = originalData.applyPatchWithActions( patch, context: context ) XCTAssertEqual(result, originalData) XCTAssertEqual(handledUrls, [URL(string: "action://failed")!]) } } private let newDivText1 = divText(text: "New text 1") private let newDivText2 = divText(text: "New text 2") private func makePager(items: [Div]) -> Div { divPager( items: items, layoutMode: .divPageSize(DivPageSize(pageWidth: DivPercentageSize(value: .value(10)))) ) } private func makeState(states: [Div?]) -> Div { divState( divId: "state", states: states.enumerated().map { index, div in divStateState(div: div, stateId: "state_\(index)") } ) } private func makeTabs(items: [Div]) -> Div { divTabs( items: items.enumerated().map { index, div in divTabsItem(div: div, title: "Tab \(index)") } ) } extension DivData { fileprivate func applyPatch( _ changes: (String, [Div]?)..., callbacks: DivPatchCallbacks = Callbacks.empty ) -> DivData { let patch = DivPatch( changes: changes.map { id, items in DivPatch.Change(id: id, items: items) } ) return applyPatch(patch, callbacks: callbacks) } fileprivate func applyPatch( _ change: (String, Div), callbacks: DivPatchCallbacks = Callbacks.empty ) -> DivData { applyPatch((change.0, [change.1]), callbacks: callbacks) } }