[Framework] Feed List improvements (#19)
This PR contains the work done to improve the overall implementation of the `FeedListViewModel` view model in the `Feed` framework by utilising unit tests. Reviewed-on: rock-n-code/app-reviews#19 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com> Co-committed-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #19.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Array+ReviewDTOs.swift
|
||||
// ReviewsFeed
|
||||
//
|
||||
// Created by Javier Cicchelli on 22/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ReviewsFeedKit
|
||||
|
||||
extension Array where Element == ReviewsFeedKit.Review {
|
||||
|
||||
// MARK: Constants
|
||||
static let none: [ReviewsFeedKit.Review] = []
|
||||
|
||||
static let sample: [ReviewsFeedKit.Review] = [
|
||||
.init(
|
||||
id: 1,
|
||||
author: "Some author name #1 here",
|
||||
title: "Some review title #1 goes here...",
|
||||
content: "Some long, long, explanatory review comment #1 goes here...",
|
||||
rating: 3,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 2,
|
||||
author: "Some author name #2 here",
|
||||
title: "Some review title #2 goes here...",
|
||||
content: "Some long, long, explanatory review comment #2 goes here...",
|
||||
rating: 5,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 3,
|
||||
author: "Some author name #3 here",
|
||||
title: "Some review title #3 goes here...",
|
||||
content: "Some long, long, explanatory review comment #3 goes here...",
|
||||
rating: 1,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 4,
|
||||
author: "Some author name #4 here",
|
||||
title: "Some review title #4 goes here...",
|
||||
content: "Some long, long, explanatory review comment #4 goes here...",
|
||||
rating: 4,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 5,
|
||||
author: "Some author name #5 here",
|
||||
title: "Some review title #5 goes here...",
|
||||
content: "Some long, long, explanatory review comment #5 goes here...",
|
||||
rating: 2,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
]
|
||||
|
||||
}
|
||||
@@ -20,14 +20,14 @@ extension FeedListViewController {
|
||||
private let topWords: TopWordsUseCase = .init()
|
||||
|
||||
// MARK: Properties
|
||||
@Published var filter: FilterOption = .all
|
||||
@Published var isFilterEnabled: Bool = false
|
||||
@Published var isFiltering: Bool = false
|
||||
@Published var isLoading: Bool = false
|
||||
@Published var state: FeedListState = .initial
|
||||
@Published private(set) var filter: FilterOption = .all
|
||||
@Published private(set) var isFilterEnabled: Bool = false
|
||||
@Published private(set) var isFiltering: Bool = false
|
||||
@Published private(set) var isLoading: Bool = false
|
||||
@Published private(set) var state: FeedListState = .initial
|
||||
|
||||
var items: [Review] = []
|
||||
var words: [TopWord] = []
|
||||
private(set) var items: [Review] = []
|
||||
private(set) var words: [TopWord] = []
|
||||
|
||||
private var reviewsAll: [Review] = []
|
||||
private var reviewsFiltered: FilteredReviews = [:]
|
||||
@@ -57,50 +57,51 @@ extension FeedListViewController {
|
||||
|
||||
var isWordsShowing: Bool {
|
||||
filter != .all
|
||||
&& !words.isEmpty
|
||||
&& !words.isEmpty
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
func fetch() {
|
||||
Task {
|
||||
isFilterEnabled = false
|
||||
isLoading = items.isEmpty
|
||||
func fetch() async {
|
||||
isFilterEnabled = false
|
||||
isLoading = items.isEmpty
|
||||
|
||||
do {
|
||||
let output = try await iTunesService.getReviews(.init(
|
||||
appID: configuration.appID,
|
||||
countryCode: configuration.countryCode
|
||||
))
|
||||
|
||||
reviewsAll = output.reviews.map(Review.init)
|
||||
reviewsFiltered = FilterOption.allCases
|
||||
.reduce(into: FilteredReviews()) { partialResult, option in
|
||||
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
|
||||
}
|
||||
reviewsTopWords = reviewsFiltered
|
||||
.mapValues { reviews in
|
||||
reviews.map(\.comment)
|
||||
.compactMap { try? filterWords($0) }
|
||||
}
|
||||
.mapValues {
|
||||
topWords($0).map(TopWord.init)
|
||||
}
|
||||
do {
|
||||
let output = try await iTunesService.getReviews(.init(
|
||||
appID: configuration.appID,
|
||||
countryCode: configuration.countryCode
|
||||
))
|
||||
|
||||
reviewsAll = output.reviews.map(Review.init)
|
||||
reviewsFiltered = FilterOption.allCases
|
||||
.reduce(into: FilteredReviews()) { partialResult, option in
|
||||
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
|
||||
}
|
||||
reviewsTopWords = reviewsFiltered
|
||||
.mapValues { reviews in
|
||||
reviews.map(\.comment)
|
||||
.compactMap { try? filterWords($0) }
|
||||
}
|
||||
.mapValues {
|
||||
topWords($0).map(TopWord.init)
|
||||
}
|
||||
|
||||
items = filter == .all
|
||||
? reviewsAll
|
||||
: reviewsFiltered[filter] ?? []
|
||||
items = filter == .all
|
||||
? reviewsAll
|
||||
: reviewsFiltered[filter] ?? []
|
||||
words = filter == .all
|
||||
? []
|
||||
: reviewsTopWords[filter] ?? []
|
||||
|
||||
isFilterEnabled = !items.isEmpty
|
||||
state = items.isEmpty
|
||||
? .empty
|
||||
: .populated
|
||||
} catch {
|
||||
items = []
|
||||
state = .error
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
isFilterEnabled = !items.isEmpty
|
||||
state = items.isEmpty
|
||||
? .empty
|
||||
: .populated
|
||||
} catch {
|
||||
items = []
|
||||
state = .error
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func filter(by option: FilterOption) {
|
||||
@@ -115,16 +116,19 @@ extension FeedListViewController {
|
||||
}
|
||||
|
||||
func item(for index: Int) -> Review? {
|
||||
guard
|
||||
!items.isEmpty,
|
||||
index < items.count
|
||||
else {
|
||||
guard !items.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return isWordsShowing
|
||||
? items[index - 1]
|
||||
: items[index]
|
||||
let indexToUse = isWordsShowing
|
||||
? index - 1
|
||||
: index
|
||||
|
||||
guard indexToUse < items.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return items[indexToUse]
|
||||
}
|
||||
|
||||
func openItem(at index: Int) {
|
||||
|
||||
@@ -118,7 +118,7 @@ final class FeedListViewController: UIViewController {
|
||||
registerTableCells()
|
||||
bindViewModel()
|
||||
|
||||
viewModel.fetch()
|
||||
Task { await viewModel.fetch() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -170,7 +170,7 @@ private extension FeedListViewController {
|
||||
|
||||
// MARK: Actions
|
||||
@objc func refresh(_ sender: AnyObject) {
|
||||
self.viewModel.fetch()
|
||||
Task { await self.viewModel.fetch() }
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
@@ -264,7 +264,7 @@ private extension FeedListViewController {
|
||||
)
|
||||
: nil,
|
||||
action: isErrorState
|
||||
? { self.viewModel.fetch() }
|
||||
? { Task { await self.viewModel.fetch() } }
|
||||
: nil
|
||||
)
|
||||
|
||||
@@ -455,53 +455,7 @@ import ReviewsiTunesKit
|
||||
#Preview("Feed List with few reviews") {
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: [
|
||||
.init(
|
||||
id: 1,
|
||||
author: "Some author name #1 here",
|
||||
title: "Some review title #1 goes here...",
|
||||
content: "Some long, explanatory review comment #1 goes here...",
|
||||
rating: 3,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 2,
|
||||
author: "Some author name #2 here",
|
||||
title: "Some review title #2 goes here...",
|
||||
content: "Some long, explanatory review comment #2 goes here...",
|
||||
rating: 5,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 3,
|
||||
author: "Some author name #3 here",
|
||||
title: "Some review title #3 goes here...",
|
||||
content: "Some long, explanatory review comment #3 goes here...",
|
||||
rating: 1,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 4,
|
||||
author: "Some author name #4 here",
|
||||
title: "Some review title #4 goes here...",
|
||||
content: "Some long, explanatory review comment #4 goes here...",
|
||||
rating: 4,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
.init(
|
||||
id: 5,
|
||||
author: "Some author name #5 here",
|
||||
title: "Some review title #5 goes here...",
|
||||
content: "Some long, explanatory review comment #5 goes here...",
|
||||
rating: 2,
|
||||
version: "v1.0.0",
|
||||
updated: .init()
|
||||
),
|
||||
])
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
return UINavigationController(rootViewController: FeedListViewController(.init(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// FeedListCoordinationSpy.swift
|
||||
// ReviewsFeedTest
|
||||
//
|
||||
// Created by Javier Cicchelli on 22/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import ReviewsFeed
|
||||
|
||||
final class FeedListCoordinationSpy: FeedListCoordination {
|
||||
|
||||
// MARK: Properties
|
||||
var itemOpened: Bool = false
|
||||
|
||||
// MARK: Functions
|
||||
func open(_ item: ReviewsFeed.Review) {
|
||||
itemOpened = true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
//
|
||||
// FeedListViewModelTests.swift
|
||||
// ReviewsFeedTests
|
||||
//
|
||||
// Created by Javier Cicchelli on 22/03/2024.
|
||||
// Copyright © 2024 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import ReviewsFoundationKit
|
||||
import ReviewsiTunesKit
|
||||
import XCTest
|
||||
|
||||
@testable import ReviewsFeed
|
||||
|
||||
final class FeedListViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: Constants
|
||||
private let coordination: FeedListCoordinationSpy = .init()
|
||||
|
||||
// MARK: Properties
|
||||
private var sut: FeedListViewController.ViewModel!
|
||||
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
// MARK: Setup
|
||||
override func setUp() async throws {
|
||||
sut = .init(
|
||||
configuration: .init(session: .mock),
|
||||
coordination: coordination
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() async throws {
|
||||
cancellables.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Initialisers tests
|
||||
func testInit() {
|
||||
// GIVEN
|
||||
// WHEN
|
||||
// THEN
|
||||
XCTAssertEqual(sut.filter, .all)
|
||||
XCTAssertFalse(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isFiltering)
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertEqual(sut.state, .initial)
|
||||
XCTAssertTrue(sut.words.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: Functions tests
|
||||
func testFetch_allItems_whenResponseOK_withSomeItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertTrue(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertFalse(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 5)
|
||||
XCTAssertEqual(sut.itemsCount, 5)
|
||||
XCTAssertEqual(sut.state, .populated)
|
||||
}
|
||||
|
||||
func testFetch_allItems_whenResponseOK_withNoItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertFalse(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertEqual(sut.state, .empty)
|
||||
}
|
||||
|
||||
func testFetch_allItems_whenResponseNotOK() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(statusCode: 404)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertFalse(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertEqual(sut.state, .error)
|
||||
}
|
||||
|
||||
func testFetch_filteredItems_whenResponseOK_withSomeItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertEqual(sut.filter, .only1Star)
|
||||
XCTAssertTrue(sut.isFilterEnabled)
|
||||
XCTAssertTrue(sut.isWordsShowing)
|
||||
XCTAssertFalse(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 1)
|
||||
XCTAssertEqual(sut.itemsCount, 2)
|
||||
XCTAssertEqual(sut.state, .populated)
|
||||
XCTAssertFalse(sut.words.isEmpty)
|
||||
XCTAssertEqual(sut.words.count, 3)
|
||||
}
|
||||
|
||||
func testFetch_filteredItems_whenResponseOK_withNoItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertEqual(sut.filter, .only1Star)
|
||||
XCTAssertFalse(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertEqual(sut.state, .empty)
|
||||
XCTAssertTrue(sut.words.isEmpty)
|
||||
}
|
||||
|
||||
func testFetch_filteredItems_whenResponseNotOK() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var isLoading: [Bool] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(statusCode: 404)
|
||||
|
||||
// WHEN
|
||||
sut.$isLoading
|
||||
.collect(3)
|
||||
.sink { value in
|
||||
isLoading.append(contentsOf: value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(isLoading, [false, true, false])
|
||||
XCTAssertEqual(sut.filter, .only1Star)
|
||||
XCTAssertFalse(sut.isFilterEnabled)
|
||||
XCTAssertFalse(sut.isWordsShowing)
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertEqual(sut.state, .error)
|
||||
XCTAssertTrue(sut.words.isEmpty)
|
||||
}
|
||||
|
||||
func testFilter_forNewOption_withSomeItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var filter: [FilterOption] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$filter
|
||||
.dropFirst()
|
||||
.sink { value in
|
||||
filter.append(value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(filter, [.only1Star])
|
||||
XCTAssertFalse(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 1)
|
||||
XCTAssertEqual(sut.itemsCount, 2)
|
||||
XCTAssertFalse(sut.words.isEmpty)
|
||||
XCTAssertEqual(sut.words.count, 3)
|
||||
}
|
||||
|
||||
func testFilter_forNewOption_withNoItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var filter: [FilterOption] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$filter
|
||||
.dropFirst()
|
||||
.sink { value in
|
||||
filter.append(value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(filter, [.only1Star])
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 0)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertTrue(sut.words.isEmpty)
|
||||
XCTAssertEqual(sut.words.count, 0)
|
||||
}
|
||||
|
||||
func testFilter_forSameOption_withSomeItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var filter: [FilterOption] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$filter
|
||||
.dropFirst()
|
||||
.sink { value in
|
||||
filter.append(value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(filter, [.only1Star])
|
||||
XCTAssertFalse(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 1)
|
||||
XCTAssertEqual(sut.itemsCount, 2)
|
||||
XCTAssertFalse(sut.words.isEmpty)
|
||||
XCTAssertEqual(sut.words.count, 3)
|
||||
}
|
||||
|
||||
func testFilter_forSameOption_withNoItems() async {
|
||||
let expectation = XCTestExpectation()
|
||||
|
||||
var filter: [FilterOption] = []
|
||||
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.$filter
|
||||
.dropFirst()
|
||||
.sink { value in
|
||||
filter.append(value)
|
||||
|
||||
expectation.fulfill()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
wait(for: [expectation], timeout: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertEqual(filter, [.only1Star])
|
||||
XCTAssertTrue(sut.items.isEmpty)
|
||||
XCTAssertEqual(sut.items.count, 0)
|
||||
XCTAssertEqual(sut.itemsCount, 0)
|
||||
XCTAssertTrue(sut.words.isEmpty)
|
||||
XCTAssertEqual(sut.words.count, 0)
|
||||
}
|
||||
|
||||
func testItemFor_index_withSomeItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
let item = sut.item(for: 0)
|
||||
|
||||
// THEN
|
||||
XCTAssertNotNil(item)
|
||||
}
|
||||
|
||||
func testItemFor_indexOutOfBounds_withSomeItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
let item = sut.item(for: 10)
|
||||
|
||||
// THEN
|
||||
XCTAssertNil(item)
|
||||
}
|
||||
|
||||
func testItemFor_index_withFilteredItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
let item = sut.item(for: 1)
|
||||
|
||||
// THEN
|
||||
XCTAssertNotNil(item)
|
||||
}
|
||||
|
||||
func testItemFor_indexOutOfBounds_withFilteredItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
let item = sut.item(for: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertNil(item)
|
||||
}
|
||||
|
||||
func testItemFor_index_withNoItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
let item = sut.item(for: 0)
|
||||
|
||||
// THEN
|
||||
XCTAssertNil(item)
|
||||
}
|
||||
|
||||
func testOpenItemAt_index_withSomeItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
sut.openItem(at: 0)
|
||||
|
||||
// THEN
|
||||
XCTAssertTrue(coordination.itemOpened)
|
||||
}
|
||||
|
||||
func testOpenItemAt_indexOutOfBounds_withSomeItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
sut.openItem(at: 10)
|
||||
|
||||
// THEN
|
||||
XCTAssertFalse(coordination.itemOpened)
|
||||
}
|
||||
|
||||
func testOpenItemAt_index_withFilteredItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.openItem(at: 1)
|
||||
|
||||
// THEN
|
||||
XCTAssertTrue(coordination.itemOpened)
|
||||
}
|
||||
|
||||
func testOpenItemAt_indexOutOfBounds_withFilteredItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .sample)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
sut.filter(by: .only1Star)
|
||||
|
||||
await sut.fetch()
|
||||
|
||||
sut.openItem(at: 2)
|
||||
|
||||
// THEN
|
||||
XCTAssertFalse(coordination.itemOpened)
|
||||
}
|
||||
|
||||
func testOpenItemAt_index_withNoItems() async {
|
||||
// GIVEN
|
||||
MockURLProtocol.response = .init(
|
||||
statusCode: 200,
|
||||
object: Feed(entries: .none)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
await sut.fetch()
|
||||
|
||||
sut.openItem(at: 0)
|
||||
|
||||
// THEN
|
||||
XCTAssertFalse(coordination.itemOpened)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user