mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
726 lines
18 KiB
Swift
726 lines
18 KiB
Swift
@testable import DivKit
|
|
import DivKitTestsSupport
|
|
@testable import LayoutKit
|
|
import VGSL
|
|
import XCTest
|
|
|
|
final class DivBaseExtensionsTests: XCTestCase {
|
|
private let timer = TestTimerScheduler()
|
|
|
|
func test_WithId() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
id: "id1"
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
accessibilityElement: accessibility(identifier: "id1")
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithMarginsAndPaddings() {
|
|
let block = makeBlock(
|
|
divText(
|
|
margins: DivEdgeInsets(bottom: .value(10), top: .value(10)),
|
|
paddings: DivEdgeInsets(left: .value(20), right: .value(20)),
|
|
text: "Hello!"
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: DecoratingBlock(
|
|
child: textBlock(
|
|
text: "Hello!",
|
|
path: .root + 0 + "text"
|
|
),
|
|
paddings: EdgeInsets(horizontal: 20),
|
|
accessibilityElement: accessibility(
|
|
traits: .staticText,
|
|
label: "Hello!"
|
|
)
|
|
),
|
|
boundary: .noClip,
|
|
paddings: EdgeInsets(vertical: 10)
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithAccessibility() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
accessibility: DivAccessibility(
|
|
description: .value("Accessibility description"),
|
|
type: .button
|
|
),
|
|
id: "id1"
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
accessibilityElement: accessibility(
|
|
traits: .button,
|
|
label: "Accessibility description",
|
|
identifier: "id1"
|
|
)
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_MarginsOverAccessibility() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
accessibility: DivAccessibility(
|
|
description: .value("Accessibility description"),
|
|
type: .button
|
|
),
|
|
margins: DivEdgeInsets(bottom: .value(10), top: .value(10))
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
accessibilityElement: accessibility(
|
|
traits: .button,
|
|
label: "Accessibility description"
|
|
)
|
|
),
|
|
boundary: .noClip,
|
|
paddings: EdgeInsets(vertical: 10)
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithActions() throws {
|
|
let context = DivBlockModelingContext.default
|
|
let actions = [
|
|
divAction(
|
|
logId: "action1_log_id",
|
|
url: "https://some.url"
|
|
),
|
|
divAction(
|
|
logId: "action2_log_id",
|
|
typed: .divActionSetVariable(DivActionSetVariable(
|
|
value: .integerValue(IntegerValue(value: .value(10))),
|
|
variableName: .value("var1")
|
|
))
|
|
),
|
|
]
|
|
|
|
let block = makeBlock(
|
|
divContainer(actions: actions)
|
|
)
|
|
|
|
let expectedBlock = try StateBlock(
|
|
child: DecoratingBlock(
|
|
child: ContainerBlock(
|
|
layoutDirection: .vertical,
|
|
children: [],
|
|
path: UIElementPath("test_card_id") + "0" + "container"
|
|
),
|
|
actions: NonEmptyArray(actions.compactMap {
|
|
$0.uiAction(
|
|
context: context
|
|
.modifying(pathSuffix: "0")
|
|
.modifying(pathSuffix: "container")
|
|
)
|
|
}),
|
|
actionAnimation: .default,
|
|
accessibilityElement: .default,
|
|
path: .root + 0 + "container"
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_MarginsOverActions() {
|
|
let action = divAction(logId: "action_log_id")
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
actions: [action],
|
|
margins: DivEdgeInsets(bottom: .value(10), top: .value(10))
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
actions: NonEmptyArray(action.uiAction(pathSuffix: "0/separator")!),
|
|
actionAnimation: .default,
|
|
accessibilityElement: .default,
|
|
path: .root + 0 + "separator"
|
|
),
|
|
boundary: .noClip,
|
|
paddings: EdgeInsets(vertical: 10),
|
|
path: nil
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithBorderAndActions() {
|
|
let action = divAction(logId: "action_log_id")
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
actions: [action],
|
|
border: DivBorder(
|
|
cornerRadius: .value(20),
|
|
stroke: DivStroke(
|
|
color: .value(color("#112233")),
|
|
width: .value(2)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
actions: NonEmptyArray(action.uiAction(pathSuffix: "0/separator")!),
|
|
actionAnimation: .default,
|
|
boundary: .clipCorner(CornerRadii(20)),
|
|
border: BlockBorder(
|
|
color: color("#112233"),
|
|
width: 2
|
|
),
|
|
accessibilityElement: .default,
|
|
path: .root + 0 + "separator"
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithShadowActionsAndMargins() {
|
|
let action = divAction(
|
|
logId: "action_log_id",
|
|
url: "https://some.url"
|
|
)
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
actions: [action],
|
|
border: DivBorder(
|
|
hasShadow: .value(true),
|
|
shadow: DivShadow(
|
|
blur: .value(5),
|
|
color: .value(color("#112233")),
|
|
offset: DivPoint(
|
|
x: DivDimension(value: .value(10)),
|
|
y: DivDimension(value: .value(20))
|
|
)
|
|
)
|
|
),
|
|
margins: DivEdgeInsets(bottom: .value(10), top: .value(10))
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: DecoratingBlock(
|
|
child: ShadedBlock(
|
|
block: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
accessibilityElement: .default,
|
|
path: nil
|
|
),
|
|
shadow: BlockShadow(
|
|
cornerRadii: CornerRadii(0),
|
|
blurRadius: 5,
|
|
offset: CGPoint(x: 10, y: 20),
|
|
color: color("#112233")
|
|
)
|
|
),
|
|
actions: NonEmptyArray(action.uiAction(pathSuffix: "0/separator")!),
|
|
actionAnimation: .default,
|
|
boundary: .noClip,
|
|
path: .root + 0 + "separator"
|
|
),
|
|
boundary: .noClip,
|
|
paddings: EdgeInsets(vertical: 10),
|
|
path: nil
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithVisibility_Gone() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
visibility: .value(.gone)
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: EmptyBlock.zeroSized,
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithVisibility_Invisible() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
visibility: .value(.invisible)
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
childAlpha: 0.0
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithAlpha() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
alpha: 0.3
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
childAlpha: 0.3,
|
|
accessibilityElement: .default
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithBackground() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
background: solidBackground(RGBAColor.red)
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
backgroundColor: RGBAColor.red,
|
|
accessibilityElement: .default
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithAlphaAndBackground() {
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
alpha: 0.3,
|
|
background: solidBackground(RGBAColor.red)
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
backgroundColor: RGBAColor.red,
|
|
accessibilityElement: .default
|
|
),
|
|
childAlpha: 0.3
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithBackgroundAndActions() {
|
|
let action = divAction(logId: "action_log_id")
|
|
let block = makeBlock(
|
|
divSeparator(
|
|
actions: [action],
|
|
background: solidBackground(color("#112233"))
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: separatorBlock(),
|
|
backgroundColor: color("#112233"),
|
|
actions: NonEmptyArray(action.uiAction(pathSuffix: "0/separator")!),
|
|
actionAnimation: .default,
|
|
accessibilityElement: .default,
|
|
path: .root + 0 + "separator"
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WithLocalVariables() {
|
|
let block = makeBlock(
|
|
divText(
|
|
textExpression: "@{local_var}",
|
|
variables: [variable("local_var", "Hello!")]
|
|
)
|
|
)
|
|
|
|
let expectedBlock = StateBlock(
|
|
child: DecoratingBlock(
|
|
child: TextBlock(
|
|
widthTrait: .resizable,
|
|
text: "Hello!".withTypo(),
|
|
verticalAlignment: .leading,
|
|
accessibilityElement: nil,
|
|
path: .root + 0 + "text"
|
|
),
|
|
accessibilityElement: accessibility(
|
|
traits: .staticText,
|
|
label: "Hello!"
|
|
)
|
|
),
|
|
ids: []
|
|
)
|
|
|
|
assertEqual(block, expectedBlock)
|
|
}
|
|
|
|
func test_WhenCreatesBlockAfterItBeingGone_ReportsVisibility() async throws {
|
|
try await expectVisibilityActionsToRun(
|
|
forVisibleBlockFile: "div-text-visibility-actions-visible",
|
|
invisibleBlockFile: "div-text-visibility-actions-gone"
|
|
)
|
|
}
|
|
|
|
func test_WhenCreatesBlockAfterItBeingInvisible_ReportsVisibility() async throws {
|
|
try await 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_card_id")
|
|
)
|
|
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)
|
|
}
|
|
|
|
@MainActor
|
|
func test_WhenBlockHasVisibilityAndDisappearAction_DoNotShareVisibilityCounter() async throws {
|
|
let context = DivBlockModelingContext(scheduler: timer)
|
|
|
|
let blockVisibleFirst = try makeBlock(
|
|
fromFile: "div-text-visibility-and-disappear-action",
|
|
context: context
|
|
)
|
|
|
|
let rect = CGRect(origin: .zero, size: CGSize(squareDimension: 20))
|
|
let view = blockVisibleFirst.makeBlockView()
|
|
var result = try await getViewVisibilityCallCount(
|
|
view: view,
|
|
rect: rect,
|
|
timerScheduler: timer
|
|
)
|
|
|
|
XCTAssertEqual(result, 1)
|
|
|
|
let invisibleRect: CGRect = .zero
|
|
result = try await getViewVisibilityCallCount(
|
|
view: view,
|
|
rect: rect,
|
|
visibilityRect: invisibleRect,
|
|
timerScheduler: timer
|
|
)
|
|
|
|
XCTAssertEqual(result, 1)
|
|
}
|
|
|
|
@MainActor
|
|
func test_WhenBlockHasVisibilityAndDisappearAction_WithCommonIds_DoNotShareVisibilityCounter(
|
|
) async throws {
|
|
let context = DivBlockModelingContext(scheduler: timer)
|
|
let commonLogId = "common_id"
|
|
|
|
let block = makeBlock(
|
|
divText(
|
|
disappearActions: [
|
|
DivDisappearAction(
|
|
disappearDuration: .value(0),
|
|
logId: .value(commonLogId),
|
|
logLimit: .value(4)
|
|
),
|
|
],
|
|
text: "arbitary text",
|
|
visibilityActions: [
|
|
DivVisibilityAction(
|
|
logId: .value(commonLogId),
|
|
logLimit: .value(2),
|
|
visibilityDuration: .value(0)
|
|
),
|
|
]
|
|
),
|
|
context: context
|
|
)
|
|
|
|
let visibilityTester = VisibilityTester(block: block, timer: timer)
|
|
try await visibilityTester.setViewVisibleAndDisappear(repeatCount: 10)
|
|
|
|
XCTAssertEqual(visibilityTester.callsCount, 6)
|
|
XCTAssertEqual(visibilityTester.callsCount(type: .appear), 2)
|
|
XCTAssertEqual(visibilityTester.callsCount(type: .disappear), 4)
|
|
}
|
|
|
|
@MainActor
|
|
func test_WhenBlockHasVisibilityActions_WithCommonIds_ShareCounterAndAddWarning() async throws {
|
|
let context = DivBlockModelingContext(scheduler: timer)
|
|
let appearId = "appear_id"
|
|
|
|
let block = makeBlock(
|
|
divText(
|
|
text: "arbitary text",
|
|
visibilityActions: [
|
|
DivVisibilityAction(
|
|
logId: .value(appearId),
|
|
logLimit: .value(1),
|
|
visibilityDuration: .value(0)
|
|
),
|
|
DivVisibilityAction(
|
|
logId: .value(appearId),
|
|
logLimit: .value(2),
|
|
url: .value(testURL),
|
|
visibilityDuration: .value(0)
|
|
),
|
|
DivVisibilityAction(
|
|
logId: .value(appearId),
|
|
logLimit: .value(3),
|
|
visibilityDuration: .value(0)
|
|
),
|
|
]
|
|
),
|
|
context: context,
|
|
ignoreErrors: true
|
|
)
|
|
|
|
let visibilityTester = VisibilityTester(block: block, timer: timer)
|
|
try await visibilityTester.setViewVisibleAndDisappear(repeatCount: 20)
|
|
|
|
XCTAssertEqual(visibilityTester.callsCount(type: .appear), 3)
|
|
|
|
let appearTestURLActionsCount = visibilityTester.callsCount(
|
|
type: .appear,
|
|
url: testURL
|
|
)
|
|
XCTAssertEqual(appearTestURLActionsCount, 1)
|
|
|
|
assertEqual(
|
|
context.errorsStorage.errors,
|
|
[
|
|
DivBlockModelingWarning(
|
|
"appear actions array contains non-unique log_id values: [Optional(\"appear_id\")]",
|
|
path: .root + 0 + "text"
|
|
),
|
|
]
|
|
)
|
|
}
|
|
|
|
@MainActor
|
|
func test_WhenBlockHasDisappearActions_WithCommonIds_ShareCounterAndAddWarning() async throws {
|
|
let context = DivBlockModelingContext(scheduler: timer)
|
|
let disappearId = "dissappear_id"
|
|
|
|
let block = makeBlock(
|
|
divText(
|
|
disappearActions: [
|
|
DivDisappearAction(
|
|
disappearDuration: .value(0),
|
|
logId: .value(disappearId),
|
|
logLimit: .value(3),
|
|
url: .value(testURL)
|
|
),
|
|
DivDisappearAction(
|
|
disappearDuration: .value(0),
|
|
logId: .value(disappearId),
|
|
logLimit: .value(1)
|
|
),
|
|
],
|
|
text: "arbitary text"
|
|
),
|
|
context: context,
|
|
ignoreErrors: true
|
|
)
|
|
|
|
let visibilityTester = VisibilityTester(block: block, timer: timer)
|
|
try await visibilityTester.setViewVisibleAndDisappear(repeatCount: 20)
|
|
|
|
XCTAssertEqual(visibilityTester.callsCount(type: .disappear), 3)
|
|
|
|
let disappearTestURLActionsCount = visibilityTester.callsCount(
|
|
type: .disappear,
|
|
url: testURL
|
|
)
|
|
XCTAssertEqual(disappearTestURLActionsCount, 3)
|
|
|
|
assertEqual(
|
|
context.errorsStorage.errors,
|
|
[
|
|
DivBlockModelingWarning(
|
|
"disappear actions array contains non-unique log_id values: [Optional(\"dissappear_id\")]",
|
|
path: .root + 0 + "text"
|
|
),
|
|
]
|
|
)
|
|
}
|
|
|
|
func test_WithBackroundReuseId() throws {
|
|
let context = DivBlockModelingContext()
|
|
let block = try makeBlock(fromFile: "div-reuse-id-background-wrapper", context: context)
|
|
|
|
XCTAssertEqual(block.reuseId, testReuseId)
|
|
}
|
|
|
|
func test_WithLottieExtensionReuseId() throws {
|
|
let block = try makeBlock(
|
|
fromFile: "div-reuse-id-lottie-wrapper",
|
|
context: DivBlockModelingContext()
|
|
)
|
|
|
|
XCTAssertEqual(block.reuseId, testReuseId)
|
|
}
|
|
|
|
func test_WithPagerItemsReuseIds() throws {
|
|
let block = try DivPagerTemplate.makeBlock(
|
|
fromFile: "div-reuse-id-pager-items",
|
|
context: DivBlockModelingContext()
|
|
) as! DecoratingBlock
|
|
let pagerBlock = block.child as! PagerBlock
|
|
|
|
let itemBlocks = pagerBlock.gallery.items.map(\.content)
|
|
for (index, itemBlock) in itemBlocks.enumerated() {
|
|
XCTAssertEqual(itemBlock.reuseId, testReuseId + String(index))
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func expectVisibilityActionsToRun(
|
|
forVisibleBlockFile file: String,
|
|
invisibleBlockFile: String
|
|
) async 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()
|
|
var result = try await getViewVisibilityCallCount(
|
|
view: view,
|
|
rect: rect,
|
|
timerScheduler: timer
|
|
)
|
|
|
|
XCTAssertEqual(result, 1)
|
|
|
|
// expect to drop lastVisibleBounds
|
|
_ = try makeBlock(fromFile: invisibleBlockFile, context: context)
|
|
let view2 = blockVisibleFirst.makeBlockView()
|
|
result = try await getViewVisibilityCallCount(
|
|
view: view2,
|
|
rect: rect,
|
|
timerScheduler: timer
|
|
)
|
|
|
|
XCTAssertEqual(result, 1)
|
|
}
|
|
}
|
|
|
|
private func makeBlock(
|
|
fromFile filename: String,
|
|
context: DivBlockModelingContext
|
|
) throws -> Block {
|
|
try DivTextTemplate.makeBlock(fromFile: filename, context: context)
|
|
}
|
|
|
|
private func assertEqual(_ actual: [DivError], _ expected: [DivError]) {
|
|
assertEqual(
|
|
actual.map(\.prettyMessage),
|
|
expected.map(\.prettyMessage)
|
|
)
|
|
}
|
|
|
|
extension TemplateValue where ResolvedValue: DivBlockModeling {
|
|
fileprivate static func makeBlock(
|
|
fromFile filename: String,
|
|
context: DivBlockModelingContext
|
|
) throws -> Block {
|
|
try make(
|
|
fromFile: filename,
|
|
subdirectory: "div-base",
|
|
context: context
|
|
)
|
|
}
|
|
}
|
|
|
|
private let testReuseId = "test_reuse_id"
|
|
private let testURL = URL(string: "https://ya.ru")!
|