support patch properties

commit_hash:8863bfcf57ccb5a8b9ea4986010f4770821772db
This commit is contained in:
babaevmm
2025-03-25 01:43:33 +03:00
parent eb9e84a9af
commit 1d68e50782
8 changed files with 180 additions and 21 deletions
@@ -58,6 +58,7 @@ public struct DivBlockModelingContext {
cardId: DivCardID,
additionalId: String? = nil,
stateManager: DivStateManager = DivStateManager(),
actionHandler: DivActionHandler? = nil,
blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(),
imageHolderFactory: DivImageHolderFactory,
extensionHandlers: [DivExtensionHandler] = [],
@@ -67,7 +68,7 @@ public struct DivBlockModelingContext {
self.init(
viewId: DivViewId(cardId: cardId, additionalId: additionalId),
stateManager: stateManager,
actionHandler: nil,
actionHandler: actionHandler,
blockStateStorage: blockStateStorage,
visibilityCounter: nil,
lastVisibleBoundsCache: nil,
@@ -1,3 +1,4 @@
import LayoutKitInterface
import VGSL
protocol DivPatchCallbacks {
@@ -19,6 +20,42 @@ extension DivData {
applyPatch(patch, callbacks: Callbacks.empty)
}
public func applyPatchWithActions(
_ patch: DivPatch,
context: DivBlockModelingContext
) -> DivData {
var changedElementIds: [String] = []
let callbacks = Callbacks(
elementChanged: { id in
changedElementIds.append(id)
}
)
let patchedData = applyPatch(patch, callbacks: callbacks)
let allChangesApplied = changedElementIds.count == patch.changes.count
let isTransactional = patch.resolveMode(context.expressionResolver) == .transactional
let isPatchApplied = !isTransactional || allChangesApplied
if isPatchApplied {
changedElementIds.forEach { id in
context.triggersStorage?.reset(elementId: id)
}
}
let actions = isPatchApplied ? patch.onAppliedActions : patch.onFailedActions
actions?.forEach { action in
context.actionHandler?.handle(
action,
path: UIElementPath(context.cardId.rawValue),
source: .callback,
sender: nil
)
}
return isPatchApplied ? patchedData : self
}
func applyPatch(_ patch: DivPatch, callbacks: DivPatchCallbacks) -> DivData {
let states = states.map {
State(div: $0.div.applySingleItemPatch(patch, callbacks: callbacks), stateId: $0.stateId)
+9 -17
View File
@@ -193,23 +193,6 @@ final class DivBlockProvider {
return
}
reasons.compactMap { $0.patch(for: self.cardId) }.forEach {
divData = divData.applyPatch(
$0,
callbacks: Callbacks(elementChanged: { [weak self] id in
self?.divKitComponents.triggersStorage.reset(elementId: id)
})
)
$0.onAppliedActions?.forEach { action in
divKitComponents.actionHandler.handle(
action,
path: UIElementPath(cardId.rawValue),
source: .callback,
sender: nil
)
}
}
self.divData = divData
let context = divKitComponents.makeContext(
cardId: cardId,
additionalId: id.additionalId,
@@ -218,6 +201,15 @@ final class DivBlockProvider {
parentScrollView: parentScrollView
)
reasons.compactMap { $0.patch(for: self.cardId) }.forEach { patch in
divData = divData.applyPatchWithActions(
patch,
context: context
)
}
self.divData = divData
if reasons.filter(\.isVariable).isEmpty {
context.layoutProviderHandler?.resetUpdatedVariables()
shouldInvokeOnVisibleBoundsChanged = true
@@ -1,4 +1,5 @@
@testable import DivKit
@testable @_spi(Internal) import DivKit
import LayoutKit
import VGSL
import XCTest
@@ -8,6 +9,20 @@ final class DivDataPatchExtensionsTests: XCTestCase {
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(
@@ -418,6 +433,113 @@ final class DivDataPatchExtensionsTests: XCTestCase {
)
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")
@@ -11,6 +11,7 @@ extension DivBlockModelingContext {
public init(
cardId: DivCardID = Self.testCardId,
additionalId: String? = nil,
actionHandler: DivActionHandler? = nil,
blockStateStorage: DivBlockStateStorage = DivBlockStateStorage(),
extensionHandlers: [DivExtensionHandler] = [],
scheduler: Scheduling? = nil,
@@ -19,6 +20,7 @@ extension DivBlockModelingContext {
self = DivBlockModelingContext(
cardId: cardId,
additionalId: additionalId,
actionHandler: actionHandler,
blockStateStorage: blockStateStorage,
imageHolderFactory: FakeImageHolderFactory(),
extensionHandlers: extensionHandlers,
+2
View File
@@ -40,6 +40,7 @@
"$description": "translations.json#/div_patch_mode",
"platforms": [
"android",
"ios",
"web"
]
},
@@ -71,6 +72,7 @@
"$description": "translations.json#/div_patch_on_failed_actions",
"platforms": [
"android",
"ios",
"web"
]
}
@@ -2867,6 +2867,7 @@
"case_id": 192,
"platforms": [
"android",
"ios",
"web"
],
"tags": [
@@ -3160,6 +3161,7 @@
],
"platforms": [
"web",
"ios",
"android"
],
"steps": [
@@ -3455,6 +3457,7 @@
"case_id": 139,
"platforms": [
"web",
"ios",
"android"
],
"tags": [
@@ -60,7 +60,7 @@
"type": "container",
"visibility_action": {
"log_id": "visibility",
"url": "div-action://download?url=https%3A%2F%2Fyastatic.net%2Fs3%2Fhome%2Fdivkit%2Ftranscation-callbacks.json"
"url": "div-action://download?url=https%3A%2F%2Fdivkit.tech%2Fplayground%2Fapi%2Fjson%3Fuuid%3Dd05dd997-ac93-426e-9dc2-ceb0e3c19183"
},
"width": {
"type": "match_parent"
@@ -72,7 +72,7 @@
"items": [
{
"type": "alert_card",
"id": "item1",
"id": "item2",
"alert_text": "1 item"
},
{