Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MBL-1814] PLOT plan selector component #2195

Merged
merged 28 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fe44ad0
PLOT Plan selector - Default state
jovaniks Nov 12, 2024
62e2ae0
Enum caps
jovaniks Nov 12, 2024
b973413
Files formated
jovaniks Nov 12, 2024
c207ab4
Adding test cases
jovaniks Nov 12, 2024
1d3a34c
More unit tests
jovaniks Nov 13, 2024
f6aa2e7
Added config function and Unit tests
jovaniks Nov 13, 2024
bfb8657
Implementing the config function to the PledgePaymentPlansViewControl…
jovaniks Nov 13, 2024
318a39d
Fixing the checkmark style to follow the figma designs
jovaniks Nov 13, 2024
27a3345
Remove Prelude and create helper functions instead. Reuse the `Pledge…
jovaniks Nov 15, 2024
152587c
Format
jovaniks Nov 15, 2024
1343f23
Fix PledgePaymentPlansDataSourceTest
jovaniks Nov 15, 2024
4ca06c3
Merge branch 'main' into jluna/MBL-1814/plot-plan-selector-component
jovaniks Nov 19, 2024
bfeb43f
Refactor: using UIStackView instead of a UITableView [MBL-1906]
jovaniks Nov 26, 2024
28ddf83
Merge branch 'jluna/MBL-1814/plot-plan-selector-component' of github.…
jovaniks Nov 26, 2024
5225a76
Merge remote-tracking branch 'oss/main' into jluna/MBL-1814/plot-plan…
jovaniks Nov 26, 2024
16830c7
Fix tests
jovaniks Nov 26, 2024
fc6ac56
PR feedback
jovaniks Nov 26, 2024
949dc63
PR Feedback
jovaniks Nov 26, 2024
1030358
Merge branch 'main' into jluna/MBL-1814/plot-plan-selector-component
jovaniks Nov 26, 2024
3d22914
PR Feedback
jovaniks Nov 26, 2024
54a8669
Restoring snapshots
jovaniks Nov 26, 2024
1259d5f
Fix stack view spacing
jovaniks Nov 26, 2024
a01864e
Fixing tests
jovaniks Nov 26, 2024
54109ce
Fix test
jovaniks Nov 26, 2024
a197ee6
Refactoring PledgePaymentPlanOptionView removing unnecessary stackviews.
jovaniks Dec 2, 2024
64e6789
Update tests snapshots
jovaniks Dec 2, 2024
44073f0
Fix snapshots
jovaniks Dec 2, 2024
56c0173
Removing PledgeDisclaimerView fix. They will be done in a new PR
jovaniks Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Library
import UIKit

protocol PledgePaymentPlansViewControllerDelegate: AnyObject {
func pledgePaymentPlansViewController(
_ viewController: PledgePaymentPlansViewController,
didSelectPaymentPlan paymentPlan: PledgePaymentPlansType
)
}

final class PledgePaymentPlansViewController: UIViewController {
// MARK: Properties

private let dataSource = PledgePaymentPlansDataSource()

private lazy var tableView: UITableView = { ContentSizeTableView(frame: .zero, style: .plain) }()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still confused about this choice - can you explain the benefit of using the table view here to me, when a stack view seems like it'd be simpler? Also, if you are sticking with a table view, thoughts on subclassing UITableViewController instead? I think it'd simplify this class a lot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in this comment thread with Scott, this approach was marked as tech debt. However, I’ve already addressed this feedback and resolved it within this PR by including a new commit that switches to a UIStackView implementation.


internal weak var delegate: PledgePaymentPlansViewControllerDelegate?

private let viewModel: PledgePaymentPlansViewModelType = PledgePaymentPlansViewModel()

// MARK: Lifecycle

override func viewDidLoad() {
super.viewDidLoad()

self.configureSubviews()
self.setupConstraints()

self.viewModel.inputs.viewDidLoad()
}

private func configureSubviews() {
self.tableView.dataSource = self.dataSource
self.tableView.delegate = self

self.view.addSubview(self.tableView)

self.tableView.registerCellClass(PledgePaymentPlanCell.self)
}

private func setupConstraints() {
self.tableView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
self.tableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.tableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}

// MARK: - Bind Styles

override func bindStyles() {
super.bindStyles()

applyWhiteBackgroundStyle(self.view)

applyTableViewStyle(self.tableView)
}

// MARK: - View model

override func bindViewModel() {
super.bindViewModel()

self.viewModel.outputs.reloadPaymentPlans.observeForUI().observeValues { [weak self] data in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: delete this extra newline

guard let self = self else { return }

self.dataSource.load(data)
self.tableView.reloadData()
}

self.viewModel.outputs.notifyDelegatePaymentPlanSelected
.observeForUI()
.observeValues { [weak self] paymentPlan in
guard let self = self else { return }

self.delegate?.pledgePaymentPlansViewController(self, didSelectPaymentPlan: paymentPlan)
}
}

// MARK: - Configuration

func configure(with value: PledgePaymentPlansAndSelectionData) {
self.viewModel.inputs.configure(with: value)
}
}

// MARK: - UITableViewDelegate

extension PledgePaymentPlansViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)

let selectedCellData = self.dataSource[indexPath] as! PledgePaymentPlanCellData

self.viewModel.inputs.didSelectRowAtIndexPath(indexPath, with: selectedCellData)
}
}

// MARK: Styles

private func applyTableViewStyle(_ tableView: UITableView) {
tableView.separatorInset = .zero
tableView.contentInsetAdjustmentBehavior = .never
tableView.isScrollEnabled = false
tableView.rowHeight = UITableView.automaticDimension

applyWhiteBackgroundStyle(tableView)
}

private func applyWhiteBackgroundStyle(_ view: UIView) {
view.backgroundColor = UIColor.ksr_white
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@testable import Kickstarter_Framework
@testable import Library
import Prelude
import SnapshotTesting
import UIKit

final class PledgePaymentPlansViewControllerTest: TestCase {
override func setUp() {
super.setUp()
AppEnvironment.pushEnvironment(mainBundle: Bundle.framework)
UIView.setAnimationsEnabled(false)
}

override func tearDown() {
AppEnvironment.popEnvironment()
UIView.setAnimationsEnabled(true)

super.tearDown()
}

func testView_PledgeInFullSelected() {
combos(Language.allLanguages, [Device.pad, Device.phone4_7inch]).forEach { language, device in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use orthogonalCombos instead of combos here - this limits the number of superfluous snapshots we have to maintain (delete the snapshots and then rerun the tests to make sure that you're only adding snapshots we're testing). Alternatively, get rid of the combos entirely and just test this in english for now - it's not like the other languages are translated yet anyways, so we're just adding a bunch of copies of the same snapshot.

The goal of snapshot tests is to comprehensively test with as few screenshots as possible. If "pledge in full" stays so simple, I'd recommend just having one or two snapshots where it's selected (all in english), and just doing orthogonalCombos for the "pledge over time" case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use orthogonalCombos and limited the languages to [Language.en] as suggested.

withEnvironment(language: language) {
let controller = PledgePaymentPlansViewController.instantiate()

let data = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeinFull)
controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)")
}
}
}

func testView_PledgeOverTimeSelected() {
combos(Language.allLanguages, [Device.pad, Device.phone4_7inch]).forEach { language, device in
withEnvironment(language: language) {
let controller = PledgePaymentPlansViewController.instantiate()

let data = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeOverTime)
controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)")
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the stack view does not look as good in the snapshot tests as the table view did (I assume that's because the view is conforming to the size we give it, which for stack view means extra space). I'm not sure if there's a good way to fix this, however, other than manually setting the snapshot height to something that makes more sense for the view. @scottkicks , do you have any good ideas for this? I'm also okay with leaving it for now - feel free to file a tech debt ticket and assign it to me and I can look into snapshot options.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Library
import UIKit

internal final class PledgePaymentPlansDataSource: ValueCellDataSource {
internal func load(_ data: PledgePaymentPlansAndSelectionData) {
self.clearValues()

let isPledgeInFullSelected = data.selectedPlan == PledgePaymentPlansType.pledgeinFull

let pledgeInFullOption = PledgePaymentPlanCellData(
type: PledgePaymentPlansType.pledgeinFull,
isSelected: isPledgeInFullSelected
)
let pledgeOverTimeOption = PledgePaymentPlanCellData(
type: PledgePaymentPlansType.pledgeOverTime,
isSelected: !isPledgeInFullSelected
)

self.set(
values: [pledgeInFullOption, pledgeOverTimeOption],
cellClass: PledgePaymentPlanCell.self,
inSection: 0
)
}

internal override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) {
switch (cell, value) {
case let (cell as PledgePaymentPlanCell, value as PledgePaymentPlanCellData):
cell.configureWith(value: value)
default:
assertionFailure("Unrecognized combo: \(cell), \(value).")
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test does a pretty good job of demonstrating why we don't need a data source here at all, since it's not testing anything useful that we're not also seeing in the snapshot tests. I still think the best option here is to get rid of the tableview entirely and just use a UIStackView. If you do want to keep the test, however, please make it a little less confusing (see further comments).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, code updated to use a UIStackView instead

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@testable import Kickstarter_Framework
@testable import KsApi
@testable import Library
import Prelude
import XCTest

final class PledgePaymentPlansDataSourceTest: XCTestCase {
private let dataSource = PledgePaymentPlansDataSource()
private let tableView = UITableView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: since the tableView isn't important to the test itself and only used to test numberOfSections, I think it makes more sense to create it on line 15 instead. If not, at least add a comment for why you're creating a random tableView in a data source test; I know I was confused.

Copy link
Contributor Author

@jovaniks jovaniks Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File removed by refactor


func testLoad_DefaultState() {
let defaultData = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeinFull)

self.dataSource.load(defaultData)

XCTAssertEqual(1, self.dataSource.numberOfSections(in: self.tableView))

XCTAssertEqual(2, self.dataSource.numberOfItems(in: PledgePaymentPlansType.pledgeinFull.rawValue))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is so confusing; why are you using PledgePaymentPlansType.pledgeinFull.rawValue to get 0 when the type has nothing to do with the section? Please do self.dataSource.numberOfItems(in: 0) or self.dataSource.numberOfItems(inSection: 0) instead. (I don't remember the exact method signature but let me know if you want help finding it!)

Copy link
Contributor Author

@jovaniks jovaniks Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File removed by refactor


XCTAssertEqual(
"PledgePaymentPlanCell",
self.dataSource.reusableId(item: 0, section: 0)
)

XCTAssertEqual(
"PledgePaymentPlanCell",
self.dataSource.reusableId(item: 1, section: 0)
)
}
jovaniks marked this conversation as resolved.
Show resolved Hide resolved
}
Loading