diff --git a/core/Sources/Components/Badge/Constants/BadgeConstants.swift b/core/Sources/Components/Badge/Constants/BadgeConstants.swift index ecb33ef00..f3d25c1a5 100644 --- a/core/Sources/Components/Badge/Constants/BadgeConstants.swift +++ b/core/Sources/Components/Badge/Constants/BadgeConstants.swift @@ -10,5 +10,8 @@ import Foundation enum BadgeConstants { static let emptySize = CGSize(width: 12, height: 12) - static let height: CGFloat = 24 + enum height { + static let normal: CGFloat = 24 + static let small: CGFloat = 16 + } } diff --git a/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift b/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift new file mode 100644 index 000000000..08635a866 --- /dev/null +++ b/core/Sources/Components/Badge/Properties/Private/BadgeSizeDependentAttributes.swift @@ -0,0 +1,24 @@ +// +// BadgeSizeDependentAttributes.swift +// SparkCore +// +// Created by michael.zimmermann on 03.08.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation +import SwiftUI + +struct BadgeSizeDependentAttributes: Equatable { + + let offset: EdgeInsets + let height: CGFloat + let font: TypographyFontToken + + static func == (lhs: BadgeSizeDependentAttributes, rhs: BadgeSizeDependentAttributes) -> Bool { + return lhs.offset == rhs.offset && + lhs.height == rhs.height && + lhs.font.font == rhs.font.font && + lhs.font.uiFont == rhs.font.uiFont + } +} diff --git a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift new file mode 100644 index 000000000..126cf122e --- /dev/null +++ b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCase.swift @@ -0,0 +1,57 @@ +// +// BadgeGetSizeUseCase.swift +// SparkCore +// +// Created by michael.zimmermann on 03.08.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +import Foundation +import SwiftUI + +// sourcery: AutoMockable +protocol BadgeGetSizeAttributesUseCaseable { + func execute(theme: Theme, size: BadgeSize) -> BadgeSizeDependentAttributes +} + +/// A use case that returns size specific attributes according to the theme +struct BadgeGetSizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable { + + // MARK: - Functions + func execute(theme: Theme, size: BadgeSize) -> BadgeSizeDependentAttributes { + return .init(offset: size.offset(spacing: theme.layout.spacing), + height: size.badgeHeight(), + font: size.font(typography: theme.typography)) + } +} + +// MARK: - Private helper extension +private extension BadgeSize { + func offset(spacing: LayoutSpacing) -> EdgeInsets { + switch self { + case .normal: return .init(vertical: spacing.small, + horizontal: spacing.medium) + case .small: return .init(vertical: 0, + horizontal: spacing.small) + } + } + + func badgeHeight() -> CGFloat { + switch self { + case .normal: + return BadgeConstants.height.normal + case .small: + return BadgeConstants.height.small + } + } + + func font(typography: Typography) -> TypographyFontToken { + switch self { + case .normal: + return typography.captionHighlight + case .small: + return typography.smallHighlight + } + } + +} diff --git a/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift new file mode 100644 index 000000000..1ebdecadf --- /dev/null +++ b/core/Sources/Components/Badge/UseCase/BadgeGetSizeAttributesUseCaseTests.swift @@ -0,0 +1,47 @@ +// +// BadgeGetSizeAttributesUseCase.swift +// SparkCoreTests +// +// Created by michael.zimmermann on 03.08.23. +// Copyright © 2023 Adevinta. All rights reserved. +// + +@testable import SparkCore +import XCTest + +final class BadgeGetSizeAttributesUseCaseTests: TestCase { + + // MARK: - Properties + var sut: BadgeGetSizeAttributesUseCase! + var theme: ThemeGeneratedMock! + + // MARK: - Setup + override func setUp() { + super.setUp() + self.theme = .mocked() + self.sut = BadgeGetSizeAttributesUseCase() + } + + // MARK: - Tests + func test_size_small() throws { + let attributes = sut.execute(theme: self.theme, size: .small) + + let expectedAttributes = BadgeSizeDependentAttributes( + offset: .init(vertical: 0, horizontal: 3), + height: 16, + font: self.theme.typography.smallHighlight) + + XCTAssertEqual(attributes, expectedAttributes) + } + + func test_size_normal() throws { + let attributes = sut.execute(theme: self.theme, size: .normal) + + let expectedAttributes = BadgeSizeDependentAttributes( + offset: .init(vertical: 3, horizontal: 5), + height: 24, + font: self.theme.typography.captionHighlight) + + XCTAssertEqual(attributes, expectedAttributes) + } +} diff --git a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift index 18d8d905c..e857a8b8c 100644 --- a/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift +++ b/core/Sources/Components/Badge/View/SwiftUI/BadgeView.swift @@ -25,8 +25,8 @@ import SwiftUI /// ``` public struct BadgeView: View { @ObservedObject private var viewModel: BadgeViewModel - @ScaledMetric private var smallOffset: CGFloat - @ScaledMetric private var mediumOffset: CGFloat + @ScaledMetric private var horizontalOffset: CGFloat + @ScaledMetric private var verticalOffset: CGFloat @ScaledMetric private var emptySize: CGFloat @ScaledMetric private var borderWidth: CGFloat @@ -45,7 +45,7 @@ public struct BadgeView: View { Text(self.viewModel.text) .font(self.viewModel.textFont.font) .foregroundColor(self.viewModel.textColor.color) - .padding(.init(vertical: self.smallOffset, horizontal: self.mediumOffset)) + .padding(.init(vertical: self.verticalOffset, horizontal: self.horizontalOffset)) .background(self.viewModel.backgroundColor.color) .border( width: self.viewModel.isBorderVisible ? borderWidth : 0, @@ -64,13 +64,13 @@ public struct BadgeView: View { let viewModel = BadgeViewModel(theme: theme, intent: intent, value: value) self.viewModel = viewModel - self._smallOffset = + self._horizontalOffset = .init(wrappedValue: - viewModel.verticalOffset + viewModel.offset.leading ) - self._mediumOffset = + self._verticalOffset = .init(wrappedValue: - viewModel.horizontalOffset + viewModel.offset.top ) self._emptySize = .init(wrappedValue: BadgeConstants.emptySize.width) self._borderWidth = .init(wrappedValue: viewModel.border.width) diff --git a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift index f63df7bb4..7ae751acc 100644 --- a/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift +++ b/core/Sources/Components/Badge/View/UIKit/BadgeUIView.swift @@ -29,9 +29,6 @@ public class BadgeUIView: UIView { // vertical and horizontal offsets private var widthConstraint: NSLayoutConstraint? private var heightConstraint: NSLayoutConstraint? - private var sizeConstraints: [NSLayoutConstraint?] { - [widthConstraint, heightConstraint] - } // Constraints for attach / detach private var attachLeadingAnchorConstraint: NSLayoutConstraint? @@ -46,22 +43,12 @@ public class BadgeUIView: UIView { // Constraints for badge text label. // All of these are applied to the badge text label - private var labelTopConstraint: NSLayoutConstraint? private var labelLeadingConstraint: NSLayoutConstraint? private var labelTrailingConstraint: NSLayoutConstraint? - private var labelBottomConstraint: NSLayoutConstraint? - - // Array of badge text label constraints for - // easier activation - private var labelConstraints: [NSLayoutConstraint?] { - [labelTopConstraint, labelLeadingConstraint, labelTrailingConstraint, labelBottomConstraint] - } // Bool property that determines wether we should // install and activate text label constraints or not private var shouldSetupLabelConstrains: Bool { - self.labelTopConstraint == nil || - self.labelBottomConstraint == nil || self.labelLeadingConstraint == nil || self.labelTrailingConstraint == nil } @@ -128,8 +115,8 @@ public class BadgeUIView: UIView { self.viewModel.isBorderVisible = newValue } } - // MARK: - Init + // MARK: - Init public init(theme: Theme, intent: BadgeIntentType, size: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isBorderVisible: Bool = true) { self.viewModel = BadgeViewModel(theme: theme, intent: intent, size: size, value: value, format: format, isBorderVisible: isBorderVisible) @@ -155,8 +142,8 @@ public class BadgeUIView: UIView { private func setupScalables() { self.emptyBadgeSize = BadgeConstants.emptySize.width - self.badgeHeight = BadgeConstants.height - self.horizontalSpacing = self.viewModel.horizontalOffset + self.badgeHeight = self.viewModel.badgeHeight + self.horizontalSpacing = self.viewModel.offset.leading self.borderWidth = self.viewModel.isBorderVisible ? self.viewModel.border.width : .zero } @@ -186,38 +173,48 @@ public class BadgeUIView: UIView { // MARK: - Layouts setup private func setupLayouts() { - let textSize = textLabel.intrinsicContentSize - - self.setupSizeConstraint(for: textSize) - self.setupBadgeConstraintsIfNeeded(for: textSize) + self.setupSizeConstraint() + self.updateLeadingConstraintsIfNeeded() + self.setupBadgeConstraintsIfNeeded() } - private func setupSizeConstraint(for textSize: CGSize) { - let widht = self.viewModel.isBadgeEmpty ? self.emptyBadgeSize : textSize.width + (self.horizontalSpacing * 2) - let height = self.viewModel.isBadgeEmpty ? self.emptyBadgeSize : self.badgeHeight + private func setupSizeConstraint() { + let badgeSize = self.viewModel.isBadgeEmpty ? self.emptyBadgeSize : self.badgeHeight - if let widthConstraint, let heightConstraint { - widthConstraint.constant = widht - heightConstraint.constant = height + if let heightConstraint = self.heightConstraint, let widthConstraint = self.widthConstraint { + widthConstraint.constant = badgeSize + heightConstraint.constant = badgeSize } else { - self.widthConstraint = self.widthAnchor.constraint(equalToConstant: widht) - self.widthConstraint?.priority = .required - self.heightConstraint = self.heightAnchor.constraint(equalToConstant: height) - self.heightConstraint?.priority = .required - NSLayoutConstraint.activate(self.sizeConstraints.compactMap({ $0 })) + let widthConstraint = self.widthAnchor.constraint(greaterThanOrEqualToConstant: badgeSize) + widthConstraint.priority = .required + let heightConstraint = self.heightAnchor.constraint(equalToConstant: badgeSize) + heightConstraint.priority = .required + NSLayoutConstraint.activate([widthConstraint, heightConstraint]) + self.widthConstraint = widthConstraint + self.heightConstraint = heightConstraint } } - private func setupBadgeConstraintsIfNeeded(for textSize: CGSize) { + private func updateLeadingConstraintsIfNeeded() { + guard let leadingConstraint = self.labelLeadingConstraint, + let trailingConstraint = self.labelTrailingConstraint else { return } + + let spacing: CGFloat = self.viewModel.isBadgeEmpty ? 0 : self.horizontalSpacing + leadingConstraint.constant = spacing + trailingConstraint.constant = -spacing + } + + private func setupBadgeConstraintsIfNeeded() { guard self.shouldSetupLabelConstrains else { return } - self.labelLeadingConstraint = self.textLabel.leadingAnchor.constraint(equalTo: leadingAnchor) - self.labelTopConstraint = self.textLabel.topAnchor.constraint(equalTo: topAnchor) - self.labelTrailingConstraint = self.textLabel.trailingAnchor.constraint(equalTo: trailingAnchor) - self.labelBottomConstraint = self.textLabel.bottomAnchor.constraint(equalTo: bottomAnchor) - NSLayoutConstraint.activate(self.labelConstraints.compactMap({ $0 })) + let labelLeadingConstraint = self.textLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: self.horizontalSpacing) + let labelTrailingConstraint = self.textLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -self.horizontalSpacing) + let centerYConstraint = self.textLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) + NSLayoutConstraint.activate([labelLeadingConstraint, labelTrailingConstraint, centerYConstraint]) + self.labelLeadingConstraint = labelLeadingConstraint + self.labelTrailingConstraint = labelTrailingConstraint } public override func layoutSubviews() { @@ -245,12 +242,16 @@ public class BadgeUIView: UIView { switch position { case .topTrailingCorner: - self.attachCenterXAnchorConstraint = self.centerXAnchor.constraint(equalTo: view.trailingAnchor) - self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint(equalTo: view.topAnchor) + self.attachCenterXAnchorConstraint = self.centerXAnchor.constraint( + equalTo: view.trailingAnchor) + self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint( + equalTo: view.topAnchor) case .trailing: - self.attachLeadingAnchorConstraint = self.leadingAnchor.constraint(equalTo: view.trailingAnchor, - constant: self.viewModel.theme.layout.spacing.small) - self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint(equalTo: view.centerYAnchor) + self.attachLeadingAnchorConstraint = self.leadingAnchor.constraint( + equalTo: view.trailingAnchor, + constant: self.viewModel.theme.layout.spacing.small) + self.attachCenterYAnchorConstraint = self.centerYAnchor.constraint( + equalTo: view.centerYAnchor) } NSLayoutConstraint.activate(self.attachConstraints.compactMap({ $0 })) @@ -263,66 +264,61 @@ extension BadgeUIView { self.subscribeToTextChanges() self.subscribeToBorderChanges() self.subscribeToColorChanges() + self.subscribeToSizeChanges() } private func subscribeToTextChanges() { self.viewModel.$text - .receive(on: DispatchQueue.main) - .sink { [weak self] text in + .subscribe(in: &self.cancellables) { [weak self] text in self?.textLabel.text = text self?.reloadUISize() self?.setupLayouts() - } - .store(in: &cancellables) + } self.viewModel.$textFont - .receive(on: DispatchQueue.main) - .sink { [weak self] textFont in + .subscribe(in: &self.cancellables) { [weak self] textFont in self?.textLabel.font = textFont.uiFont self?.reloadUISize() self?.setupLayouts() } - .store(in: &cancellables) self.viewModel.$isBadgeEmpty - .receive(on: DispatchQueue.main) - .sink { [weak self] isBadgeOutlined in + .subscribe(in: &self.cancellables) { [weak self] isBadgeOutlined in self?.textLabel.text = self?.viewModel.text self?.reloadUISize() self?.setupLayouts() } - .store(in: &cancellables) } private func subscribeToBorderChanges() { self.viewModel.$isBorderVisible - .receive(on: DispatchQueue.main) - .sink { [weak self] isBadgeOutlined in + .subscribe(in: &self.cancellables) { [weak self] isBadgeOutlined in guard let self else { return } self.updateBorder(self.viewModel.border) } - .store(in: &cancellables) self.viewModel.$border - .receive(on: DispatchQueue.main) - .sink { [weak self] badgeBorder in + .subscribe(in: &self.cancellables) { [weak self] badgeBorder in self?.updateBorder(badgeBorder) } - .store(in: &cancellables) } private func subscribeToColorChanges() { self.viewModel.$textColor - .receive(on: DispatchQueue.main) - .sink { [weak self] textColor in + .subscribe(in: &self.cancellables) { [weak self] textColor in self?.textLabel.textColor = textColor.uiColor } - .store(in: &cancellables) self.viewModel.$backgroundColor - .receive(on: DispatchQueue.main) - .sink { [weak self] backgroundColor in + .subscribe(in: &self.cancellables) { [weak self] backgroundColor in self?.backgroundColor = backgroundColor.uiColor } - .store(in: &cancellables) + } + + private func subscribeToSizeChanges() { + self.viewModel.$badgeHeight + .subscribe(in: &self.cancellables) { [weak self] badgeHeight in + self?.badgeHeight = badgeHeight + self?.setupLayouts() + } } } @@ -348,11 +344,8 @@ extension BadgeUIView { } private func reloadUISize() { - if self.viewModel.isBadgeEmpty { - self._emptyBadgeSize.update(traitCollection: self.traitCollection) - } else { - self._horizontalSpacing.update(traitCollection: self.traitCollection) - } + self._emptyBadgeSize.update(traitCollection: self.traitCollection) + self._horizontalSpacing.update(traitCollection: self.traitCollection) self._badgeHeight.update(traitCollection: traitCollection) } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift index f8ce2c90a..9a6a2ecbb 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModel.swift @@ -31,28 +31,29 @@ final class BadgeViewModel: ObservableObject { // MARK: - Badge Configuration Public Properties var value: Int? = nil { didSet { - updateText() + self.updateText() } } var intent: BadgeIntentType { didSet { - updateColors() + self.updateColors() } } var size: BadgeSize { didSet { - updateFont() + self.updateFont() + self.updateScalings() } } var format: BadgeFormat { didSet { - updateText() + self.updateText() } } var theme: Theme { didSet { - updateColors() - updateScalings() + self.updateColors() + self.updateScalings() } } @@ -64,38 +65,34 @@ final class BadgeViewModel: ObservableObject { @Published var backgroundColor: any ColorToken @Published var border: BadgeBorder @Published var isBorderVisible: Bool + @Published var badgeHeight: CGFloat + @Published var offset: EdgeInsets // MARK: - Internal Appearance Properties var colorsUseCase: BadgeGetIntentColorsUseCaseable - var verticalOffset: CGFloat - var horizontalOffset: CGFloat - + var sizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable // MARK: - Initializer - init(theme: Theme, intent: BadgeIntentType, size: BadgeSize = .normal, value: Int? = nil, format: BadgeFormat = .default, isBorderVisible: Bool = true, colorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase()) { + init(theme: Theme, + intent: BadgeIntentType, + size: BadgeSize = .normal, + value: Int? = nil, + format: BadgeFormat = .default, + isBorderVisible: Bool = true, + colorsUseCase: BadgeGetIntentColorsUseCaseable = BadgeGetIntentColorsUseCase(), + sizeAttributesUseCase: BadgeGetSizeAttributesUseCaseable = BadgeGetSizeAttributesUseCase() + ) { let colors = colorsUseCase.execute(intentType: intent, on: theme.colors) self.value = value self.text = format.text(value) self.isBadgeEmpty = format.text(value).isEmpty - switch size { - case .normal: - self.textFont = theme.typography.captionHighlight - case .small: - self.textFont = theme.typography.smallHighlight - } self.textColor = colors.foregroundColor self.backgroundColor = colors.backgroundColor - let verticalOffset = theme.layout.spacing.small - let horizontalOffset = theme.layout.spacing.medium - - self.verticalOffset = verticalOffset - self.horizontalOffset = horizontalOffset - self.border = BadgeBorder( width: theme.border.width.medium, radius: theme.border.radius.full, @@ -109,15 +106,19 @@ final class BadgeViewModel: ObservableObject { self.intent = intent self.isBorderVisible = isBorderVisible self.colorsUseCase = colorsUseCase + self.sizeAttributesUseCase = sizeAttributesUseCase + + let sizeAttributes = sizeAttributesUseCase.execute(theme: theme, size: size) + self.textFont = sizeAttributes.font + self.badgeHeight = sizeAttributes.height + self.offset = sizeAttributes.offset } private func updateColors() { let colors = self.colorsUseCase.execute(intentType: self.intent, on: self.theme.colors) self.textColor = colors.foregroundColor - self.backgroundColor = colors.backgroundColor - self.border.setColor(colors.borderColor) } @@ -127,21 +128,14 @@ final class BadgeViewModel: ObservableObject { } private func updateFont() { - switch size { - case .normal: - self.textFont = self.theme.typography.captionHighlight - case .small: - self.textFont = self.theme.typography.smallHighlight - } + let sizeAttributes = self.sizeAttributesUseCase.execute(theme: self.theme, size: self.size) + self.textFont = sizeAttributes.font } private func updateScalings() { - let verticalOffset = self.theme.layout.spacing.small - let horizontalOffset = self.theme.layout.spacing.medium - - self.verticalOffset = verticalOffset - self.horizontalOffset = horizontalOffset - + let sizeAttributes = self.sizeAttributesUseCase.execute(theme: self.theme, size: self.size) + self.offset = sizeAttributes.offset self.border.setWidth(self.theme.border.width.medium) + self.badgeHeight = sizeAttributes.height } } diff --git a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift index 1d13d0e2a..88a03f55a 100644 --- a/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift +++ b/core/Sources/Components/Badge/ViewModel/BadgeViewModelTests.swift @@ -14,6 +14,7 @@ import XCTest final class BadgeViewModelTests: XCTestCase { var theme: ThemeGeneratedMock = ThemeGeneratedMock.mocked() + var subscriptions = Set() // MARK: - Tests func test_init() throws { @@ -92,6 +93,104 @@ final class BadgeViewModelTests: XCTestCase { } } + func test_theme_change_publishes_values() { + // Given + let sut = BadgeViewModel(theme: self.theme, intent: .danger) + let updateExpectation = expectation(description: "Attributes updated") + updateExpectation.expectedFulfillmentCount = 2 + + let publishers = Publishers.Zip4(sut.$offset, + sut.$textColor, + sut.$backgroundColor, + sut.$border) + + publishers.sink { _ in + updateExpectation.fulfill() + }.store(in: &self.subscriptions) + + // When + sut.theme = ThemeGeneratedMock.mocked() + + // Then + wait(for: [updateExpectation], timeout: 0.1) + } + + func test_size_change_publishes_values() { + // Given + let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .normal) + let updateExpectation = expectation(description: "Attributes updated") + updateExpectation.expectedFulfillmentCount = 2 + + let publishers = Publishers.Zip3(sut.$textFont, + sut.$badgeHeight, + sut.$offset) + + publishers.sink { _ in + updateExpectation.fulfill() + }.store(in: &self.subscriptions) + + // When + sut.size = .small + + // Then + wait(for: [updateExpectation], timeout: 0.1) + } + + func test_intent_change_publishes_values() { + // Given + let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .normal) + let updateExpectation = expectation(description: "Attributes updated") + updateExpectation.expectedFulfillmentCount = 2 + + let publishers = Publishers.Zip(sut.$textColor, + sut.$backgroundColor) + + publishers.sink { _ in + updateExpectation.fulfill() + }.store(in: &self.subscriptions) + + // When + sut.intent = .alert + + // Then + wait(for: [updateExpectation], timeout: 0.1) + } + + func test_value_change_publishes_values() { + // Given + let sut = BadgeViewModel(theme: self.theme, intent: .danger, size: .normal, value: 9) + let updateExpectation = expectation(description: "Attributes updated") + updateExpectation.expectedFulfillmentCount = 2 + + sut.$text.sink { _ in + updateExpectation.fulfill() + }.store(in: &self.subscriptions) + + // When + sut.value = 99 + + // Then + wait(for: [updateExpectation], timeout: 0.1) + } + + func test_formater_change_publishes_values() { + // Given + let sut = BadgeViewModel(theme: self.theme, intent: .danger, value: 9999, format: .default) + let updateExpectation = expectation(description: "Attributes updated") + updateExpectation.expectedFulfillmentCount = 2 + + sut.$text.sink { _ in + updateExpectation.fulfill() + }.store(in: &self.subscriptions) + + // When + sut.format = .overflowCounter(maxValue: 99) + + // Then + wait(for: [updateExpectation], timeout: 0.1) + } + + // MARK: - Private functions private func randomizeIntentAndExceptingCurrent(_ currentIntentType: BadgeIntentType) -> BadgeIntentType { let filteredIntentTypes = BadgeIntentType.allCases.filter({ $0 != currentIntentType }) let randomIndex = Int.random(in: 0...filteredIntentTypes.count - 1) @@ -100,6 +199,7 @@ final class BadgeViewModelTests: XCTestCase { } } +// MARK: - Private extensions private extension BadgeBorder { func isEqual(to theme: Theme, isOutlined: Bool) -> Bool { return (isOutlined ? width == theme.border.width.medium : width == theme.border.width.none) &&