[Framework] Feed List improvements #19
@ -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()
|
private let topWords: TopWordsUseCase = .init()
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
@Published var filter: FilterOption = .all
|
@Published private(set) var filter: FilterOption = .all
|
||||||
@Published var isFilterEnabled: Bool = false
|
@Published private(set) var isFilterEnabled: Bool = false
|
||||||
@Published var isFiltering: Bool = false
|
@Published private(set) var isFiltering: Bool = false
|
||||||
@Published var isLoading: Bool = false
|
@Published private(set) var isLoading: Bool = false
|
||||||
@Published var state: FeedListState = .initial
|
@Published private(set) var state: FeedListState = .initial
|
||||||
|
|
||||||
var items: [Review] = []
|
private(set) var items: [Review] = []
|
||||||
var words: [TopWord] = []
|
private(set) var words: [TopWord] = []
|
||||||
|
|
||||||
private var reviewsAll: [Review] = []
|
private var reviewsAll: [Review] = []
|
||||||
private var reviewsFiltered: FilteredReviews = [:]
|
private var reviewsFiltered: FilteredReviews = [:]
|
||||||
@ -57,50 +57,51 @@ extension FeedListViewController {
|
|||||||
|
|
||||||
var isWordsShowing: Bool {
|
var isWordsShowing: Bool {
|
||||||
filter != .all
|
filter != .all
|
||||||
&& !words.isEmpty
|
&& !words.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
func fetch() {
|
func fetch() async {
|
||||||
Task {
|
isFilterEnabled = false
|
||||||
isFilterEnabled = false
|
isLoading = items.isEmpty
|
||||||
isLoading = items.isEmpty
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let output = try await iTunesService.getReviews(.init(
|
let output = try await iTunesService.getReviews(.init(
|
||||||
appID: configuration.appID,
|
appID: configuration.appID,
|
||||||
countryCode: configuration.countryCode
|
countryCode: configuration.countryCode
|
||||||
))
|
))
|
||||||
|
|
||||||
reviewsAll = output.reviews.map(Review.init)
|
reviewsAll = output.reviews.map(Review.init)
|
||||||
reviewsFiltered = FilterOption.allCases
|
reviewsFiltered = FilterOption.allCases
|
||||||
.reduce(into: FilteredReviews()) { partialResult, option in
|
.reduce(into: FilteredReviews()) { partialResult, option in
|
||||||
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
|
partialResult[option] = reviewsAll.filter { $0.rating.stars == option.rawValue }
|
||||||
}
|
}
|
||||||
reviewsTopWords = reviewsFiltered
|
reviewsTopWords = reviewsFiltered
|
||||||
.mapValues { reviews in
|
.mapValues { reviews in
|
||||||
reviews.map(\.comment)
|
reviews.map(\.comment)
|
||||||
.compactMap { try? filterWords($0) }
|
.compactMap { try? filterWords($0) }
|
||||||
}
|
}
|
||||||
.mapValues {
|
.mapValues {
|
||||||
topWords($0).map(TopWord.init)
|
topWords($0).map(TopWord.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
items = filter == .all
|
items = filter == .all
|
||||||
? reviewsAll
|
? reviewsAll
|
||||||
: reviewsFiltered[filter] ?? []
|
: reviewsFiltered[filter] ?? []
|
||||||
|
words = filter == .all
|
||||||
|
? []
|
||||||
|
: reviewsTopWords[filter] ?? []
|
||||||
|
|
||||||
isFilterEnabled = !items.isEmpty
|
isFilterEnabled = !items.isEmpty
|
||||||
state = items.isEmpty
|
state = items.isEmpty
|
||||||
? .empty
|
? .empty
|
||||||
: .populated
|
: .populated
|
||||||
} catch {
|
} catch {
|
||||||
items = []
|
items = []
|
||||||
state = .error
|
state = .error
|
||||||
}
|
|
||||||
|
|
||||||
isLoading = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func filter(by option: FilterOption) {
|
func filter(by option: FilterOption) {
|
||||||
@ -115,16 +116,19 @@ extension FeedListViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func item(for index: Int) -> Review? {
|
func item(for index: Int) -> Review? {
|
||||||
guard
|
guard !items.isEmpty else {
|
||||||
!items.isEmpty,
|
|
||||||
index < items.count
|
|
||||||
else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return isWordsShowing
|
let indexToUse = isWordsShowing
|
||||||
? items[index - 1]
|
? index - 1
|
||||||
: items[index]
|
: index
|
||||||
|
|
||||||
|
guard indexToUse < items.count else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return items[indexToUse]
|
||||||
}
|
}
|
||||||
|
|
||||||
func openItem(at index: Int) {
|
func openItem(at index: Int) {
|
||||||
|
@ -118,7 +118,7 @@ final class FeedListViewController: UIViewController {
|
|||||||
registerTableCells()
|
registerTableCells()
|
||||||
bindViewModel()
|
bindViewModel()
|
||||||
|
|
||||||
viewModel.fetch()
|
Task { await viewModel.fetch() }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ private extension FeedListViewController {
|
|||||||
|
|
||||||
// MARK: Actions
|
// MARK: Actions
|
||||||
@objc func refresh(_ sender: AnyObject) {
|
@objc func refresh(_ sender: AnyObject) {
|
||||||
self.viewModel.fetch()
|
Task { await self.viewModel.fetch() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
@ -264,7 +264,7 @@ private extension FeedListViewController {
|
|||||||
)
|
)
|
||||||
: nil,
|
: nil,
|
||||||
action: isErrorState
|
action: isErrorState
|
||||||
? { self.viewModel.fetch() }
|
? { Task { await self.viewModel.fetch() } }
|
||||||
: nil
|
: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -455,53 +455,7 @@ import ReviewsiTunesKit
|
|||||||
#Preview("Feed List with few reviews") {
|
#Preview("Feed List with few reviews") {
|
||||||
MockURLProtocol.response = .init(
|
MockURLProtocol.response = .init(
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
object: Feed(entries: [
|
object: Feed(entries: .sample)
|
||||||
.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()
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return UINavigationController(rootViewController: FeedListViewController(.init(
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,6 +18,10 @@
|
|||||||
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; };
|
02909E792BAB6B0200710E14 /* FilterOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E782BAB6B0200710E14 /* FilterOption.swift */; };
|
||||||
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; };
|
02909E7B2BAB6D2E00710E14 /* Bundle+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */; };
|
||||||
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; };
|
02909E7D2BAB7FFE00710E14 /* Review+DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */; };
|
||||||
|
02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */; platformFilter = ios; };
|
||||||
|
02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */; };
|
||||||
|
02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */; };
|
||||||
|
02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */; };
|
||||||
02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; };
|
02C1B1972BAC9BFE001781DE /* FeedListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */; };
|
||||||
02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; };
|
02C1B1A92BACA722001781DE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C1B1A82BACA722001781DE /* AppCoordinator.swift */; };
|
||||||
02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; };
|
02DA924E2BAAE3FD00C47985 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */; };
|
||||||
@ -40,6 +44,13 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 02DC7F8E2BA51793000EEEBE;
|
||||||
|
remoteInfo = Feed;
|
||||||
|
};
|
||||||
02DC7FA02BA51793000EEEBE /* PBXContainerItemProxy */ = {
|
02DC7FA02BA51793000EEEBE /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */;
|
containerPortal = 345AD11024C6EDD9004E2EE1 /* Project object */;
|
||||||
@ -75,6 +86,10 @@
|
|||||||
02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; };
|
02909E782BAB6B0200710E14 /* FilterOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterOption.swift; sourceTree = "<group>"; };
|
||||||
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; };
|
02909E7A2BAB6D2E00710E14 /* Bundle+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Constants.swift"; sourceTree = "<group>"; };
|
||||||
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; };
|
02909E7C2BAB7FFE00710E14 /* Review+DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Review+DTOs.swift"; sourceTree = "<group>"; };
|
||||||
|
02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+ReviewDTOs.swift"; sourceTree = "<group>"; };
|
||||||
|
02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinationSpy.swift; sourceTree = "<group>"; };
|
||||||
02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = "<group>"; };
|
02C1B1962BAC9BFE001781DE /* FeedListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListCoordinator.swift; sourceTree = "<group>"; };
|
||||||
02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
02C1B1A82BACA722001781DE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
|
||||||
02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
02DA924D2BAAE3FD00C47985 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
@ -97,6 +112,14 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
02B36F762BAD9D1A00F1A89D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
02B36F7D2BAD9D1A00F1A89D /* ReviewsFeed.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
02DC7F8C2BA51793000EEEBE /* Frameworks */ = {
|
02DC7F8C2BA51793000EEEBE /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -217,6 +240,7 @@
|
|||||||
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */,
|
02DC7F912BA51793000EEEBE /* ReviewsFeed.h */,
|
||||||
02DA924B2BAAE3E500C47985 /* Resources */,
|
02DA924B2BAAE3E500C47985 /* Resources */,
|
||||||
02DC7FB02BA51B4F000EEEBE /* Sources */,
|
02DC7FB02BA51B4F000EEEBE /* Sources */,
|
||||||
|
02B36F862BADB1FD00F1A89D /* Previews */,
|
||||||
);
|
);
|
||||||
path = Bundle;
|
path = Bundle;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -224,10 +248,68 @@
|
|||||||
02A6DA302BA5929F00B943E2 /* Test */ = {
|
02A6DA302BA5929F00B943E2 /* Test */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
02B36F8A2BADCA5B00F1A89D /* Helpers */,
|
||||||
|
02B36F742BAD9C4500F1A89D /* Tests */,
|
||||||
);
|
);
|
||||||
path = Test;
|
path = Test;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
02B36F742BAD9C4500F1A89D /* Tests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F832BAD9DC700F1A89D /* View Models */,
|
||||||
|
);
|
||||||
|
path = Tests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F832BAD9DC700F1A89D /* View Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F842BAD9DDB00F1A89D /* FeedListViewModelTests.swift */,
|
||||||
|
);
|
||||||
|
path = "View Models";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F862BADB1FD00F1A89D /* Previews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F872BADB23200F1A89D /* Extensions */,
|
||||||
|
);
|
||||||
|
path = Previews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F872BADB23200F1A89D /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F882BADB26C00F1A89D /* Array+ReviewDTOs.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F8A2BADCA5B00F1A89D /* Helpers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F8B2BADCA6200F1A89D /* Spies */,
|
||||||
|
);
|
||||||
|
path = Helpers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F8B2BADCA6200F1A89D /* Spies */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F8C2BADCA6A00F1A89D /* Coordination */,
|
||||||
|
);
|
||||||
|
path = Spies;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
02B36F8C2BADCA6A00F1A89D /* Coordination */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
02B36F8D2BADCA8000F1A89D /* FeedListCoordinationSpy.swift */,
|
||||||
|
);
|
||||||
|
path = Coordination;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
02C1B1952BAC9BE7001781DE /* Coordinators */ = {
|
02C1B1952BAC9BE7001781DE /* Coordinators */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -364,6 +446,7 @@
|
|||||||
children = (
|
children = (
|
||||||
345AD11824C6EDD9004E2EE1 /* Reviews.app */,
|
345AD11824C6EDD9004E2EE1 /* Reviews.app */,
|
||||||
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */,
|
02DC7F8F2BA51793000EEEBE /* ReviewsFeed.framework */,
|
||||||
|
02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -391,6 +474,24 @@
|
|||||||
/* End PBXHeadersBuildPhase section */
|
/* End PBXHeadersBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
02B36F782BAD9D1A00F1A89D /* FeedTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
02B36F752BAD9D1A00F1A89D /* Sources */,
|
||||||
|
02B36F762BAD9D1A00F1A89D /* Frameworks */,
|
||||||
|
02B36F772BAD9D1A00F1A89D /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = FeedTests;
|
||||||
|
productName = ReviewsFeedTests;
|
||||||
|
productReference = 02B36F792BAD9D1A00F1A89D /* FeedTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
02DC7F8E2BA51793000EEEBE /* Feed */ = {
|
02DC7F8E2BA51793000EEEBE /* Feed */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */;
|
buildConfigurationList = 02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */;
|
||||||
@ -445,6 +546,9 @@
|
|||||||
LastUpgradeCheck = 1530;
|
LastUpgradeCheck = 1530;
|
||||||
ORGANIZATIONNAME = "Röck+Cöde";
|
ORGANIZATIONNAME = "Röck+Cöde";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
02B36F782BAD9D1A00F1A89D = {
|
||||||
|
CreatedOnToolsVersion = 15.3;
|
||||||
|
};
|
||||||
02DC7F8E2BA51793000EEEBE = {
|
02DC7F8E2BA51793000EEEBE = {
|
||||||
CreatedOnToolsVersion = 15.3;
|
CreatedOnToolsVersion = 15.3;
|
||||||
};
|
};
|
||||||
@ -466,13 +570,21 @@
|
|||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
02DC7F8E2BA51793000EEEBE /* Feed */,
|
|
||||||
345AD11724C6EDD9004E2EE1 /* App */,
|
345AD11724C6EDD9004E2EE1 /* App */,
|
||||||
|
02DC7F8E2BA51793000EEEBE /* Feed */,
|
||||||
|
02B36F782BAD9D1A00F1A89D /* FeedTests */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
02B36F772BAD9D1A00F1A89D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
02DC7F8D2BA51793000EEEBE /* Resources */ = {
|
02DC7F8D2BA51793000EEEBE /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@ -493,11 +605,21 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
02B36F752BAD9D1A00F1A89D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
02B36F852BAD9DDB00F1A89D /* FeedListViewModelTests.swift in Sources */,
|
||||||
|
02B36F8F2BADCABC00F1A89D /* FeedListCoordinationSpy.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
02DC7F8B2BA51793000EEEBE /* Sources */ = {
|
02DC7F8B2BA51793000EEEBE /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
02EACF302BABA50D00FF8ECD /* TopWordsCell.swift in Sources */,
|
02EACF302BABA50D00FF8ECD /* TopWordsCell.swift in Sources */,
|
||||||
|
02B36F892BADB26C00F1A89D /* Array+ReviewDTOs.swift in Sources */,
|
||||||
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */,
|
02620B8C2BA89C9A00DE7137 /* FeedListViewModel.swift in Sources */,
|
||||||
02EACF342BABB28900FF8ECD /* TopWord.swift in Sources */,
|
02EACF342BABB28900FF8ECD /* TopWord.swift in Sources */,
|
||||||
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
|
023AC7FC2BAA3EC10027D064 /* Int+Constants.swift in Sources */,
|
||||||
@ -532,6 +654,12 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXTargetDependency section */
|
/* Begin PBXTargetDependency section */
|
||||||
|
02B36F7F2BAD9D1A00F1A89D /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
platformFilter = ios;
|
||||||
|
target = 02DC7F8E2BA51793000EEEBE /* Feed */;
|
||||||
|
targetProxy = 02B36F7E2BAD9D1A00F1A89D /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
02DC7FA12BA51793000EEEBE /* PBXTargetDependency */ = {
|
02DC7FA12BA51793000EEEBE /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = 02DC7F8E2BA51793000EEEBE /* Feed */;
|
target = 02DC7F8E2BA51793000EEEBE /* Feed */;
|
||||||
@ -551,6 +679,59 @@
|
|||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
02B36F812BAD9D1A00F1A89D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
02B36F822BAD9D1A00F1A89D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.assignment.ing.framework.feed.test.unit";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
02DC7FA52BA51793000EEEBE /* Debug */ = {
|
02DC7FA52BA51793000EEEBE /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@ -562,6 +743,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews;
|
||||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
DYLIB_CURRENT_VERSION = 1;
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
@ -611,6 +793,7 @@
|
|||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = Frameworks/Feed/Bundle/Previews;
|
||||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
DYLIB_CURRENT_VERSION = 1;
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
@ -825,6 +1008,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
02B36F802BAD9D1A00F1A89D /* Build configuration list for PBXNativeTarget "FeedTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
02B36F812BAD9D1A00F1A89D /* Debug */,
|
||||||
|
02B36F822BAD9D1A00F1A89D /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */ = {
|
02DC7FA92BA51793000EEEBE /* Build configuration list for PBXNativeTarget "Feed" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user