-
Notifications
You must be signed in to change notification settings - Fork 19
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
Check favorite sessions in Schedule View #41
base: main
Are you sure you want to change the base?
Changes from all commits
875b195
33888d9
08a2b0d
c32c5bf
cf49338
5a32df4
846098f
23983b5
a7a611b
0e8b403
daa7fa7
1a5419a
35b4d14
f2622b8
b648df2
a929586
4e279e4
1db51b5
d3a49ab
04cefc0
d3b9901
cfb2fa3
77b7a09
0e3b9f8
6cca9bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import Dependencies | ||
import DependenciesMacros | ||
import SharedModels | ||
import Foundation | ||
|
||
public typealias Favorites = [String: [Session]] | ||
|
||
@DependencyClient | ||
public struct FileClient { | ||
public var loadFavorites: @Sendable () throws -> Favorites | ||
public var saveFavorites: @Sendable (Favorites) throws -> Void | ||
} | ||
|
||
extension FileClient: DependencyKey { | ||
static public var liveValue: FileClient = .init( | ||
loadFavorites: { | ||
guard let saveData = serialize(from: "Favorites") else { | ||
return [:] | ||
} | ||
let response = try jsonDecoder.decode(Favorites.self, from: saveData) | ||
return response | ||
}, | ||
saveFavorites: { favorites in | ||
guard let data = try? jsonEncoder.encode(favorites) else { | ||
return | ||
} | ||
deserialize(data: data, into: "Favorites") | ||
} | ||
) | ||
|
||
static func deserialize(data : Data, into filePath: String) { | ||
guard let documentPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { | ||
return | ||
} | ||
let fileURL = documentPath.appendingPathComponent(filePath + ".json") | ||
try? data.write(to: fileURL) | ||
} | ||
|
||
static func serialize(from filePath: String) -> Data? { | ||
guard let documentPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { | ||
return nil | ||
} | ||
let fileURL = documentPath.appendingPathComponent(filePath + ".json") | ||
return try? Data(contentsOf: fileURL) | ||
} | ||
} | ||
|
||
let jsonEncoder = { | ||
$0.dateEncodingStrategy = .iso8601 | ||
$0.keyEncodingStrategy = .convertToSnakeCase | ||
return $0 | ||
}(JSONEncoder()) | ||
|
||
let jsonDecoder = { | ||
$0.dateDecodingStrategy = .iso8601 | ||
$0.keyDecodingStrategy = .convertFromSnakeCase | ||
return $0 | ||
}(JSONDecoder()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,8 @@ | ||
import Foundation | ||
import SharedModels | ||
import FileClient | ||
|
||
@testable import ScheduleFeature | ||
|
||
extension Conference { | ||
static let mock1 = Self( | ||
|
@@ -95,3 +98,7 @@ extension Speaker { | |
] | ||
) | ||
} | ||
|
||
extension Favorites { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can add mock under the your test code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I moved these specific mocks near related tests in 4e279e4. |
||
static let mock1 = [Conference.mock1.title: [Session.mock1]] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,28 @@ | ||
import ComposableArchitecture | ||
import DataClient | ||
import FileClient | ||
import SharedModels | ||
import XCTest | ||
|
||
@testable import ScheduleFeature | ||
|
||
final class ScheduleTests: XCTestCase { | ||
@MainActor | ||
func testFetchData() async { | ||
func testOnAppear() async { | ||
let store = TestStore(initialState: Schedule.State()) { | ||
Schedule() | ||
} withDependencies: { | ||
$0[DataClient.self].fetchDay1 = { @Sendable in .mock1 } | ||
$0[DataClient.self].fetchDay2 = { @Sendable in .mock2 } | ||
$0[DataClient.self].fetchWorkshop = { @Sendable in .mock3 } | ||
$0[FileClient.self].loadFavorites = { @Sendable in .mock1 } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be separated your test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sorry that I can't image way to separate. Current implementation, loading favorites using I could only image to separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NO. This effects to current test case. Should be created another test case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since fetching data using |
||
} | ||
await store.send(.view(.onAppear)) | ||
await store.receive(\.fetchResponse.success) { | ||
$0.day1 = .mock1 | ||
$0.day2 = .mock2 | ||
$0.workshop = .mock3 | ||
$0.favorites = .mock1 | ||
} | ||
} | ||
|
||
|
@@ -31,8 +35,74 @@ final class ScheduleTests: XCTestCase { | |
$0[DataClient.self].fetchDay1 = { @Sendable in throw FetchError() } | ||
$0[DataClient.self].fetchDay2 = { @Sendable in .mock2 } | ||
$0[DataClient.self].fetchWorkshop = { @Sendable in .mock3 } | ||
$0[FileClient.self].loadFavorites = { @Sendable in .mock1} | ||
} | ||
await store.send(.view(.onAppear)) | ||
await store.receive(\.fetchResponse.failure) | ||
} | ||
|
||
@MainActor | ||
func testLoadFavoritesFailure() async { | ||
struct LoadError: Equatable, Error {} | ||
let store = TestStore(initialState: Schedule.State()) { | ||
Schedule() | ||
} withDependencies: { | ||
$0[DataClient.self].fetchDay1 = { @Sendable in .mock1 } | ||
$0[DataClient.self].fetchDay2 = { @Sendable in .mock2 } | ||
$0[DataClient.self].fetchWorkshop = { @Sendable in .mock3 } | ||
$0[FileClient.self].loadFavorites = { @Sendable in throw LoadError() } | ||
} | ||
await store.send(.view(.onAppear)) | ||
await store.receive(\.fetchResponse.failure) | ||
} | ||
|
||
@MainActor | ||
func testAddingFavorites() async { | ||
let initialState: ScheduleFeature.Schedule.State = ScheduleTests.selectingDay1ScheduleWithNoFavorites | ||
let firstSession = initialState.day1!.schedules.first!.sessions.first! | ||
let firstSessionFavorited = [initialState.day1!.title: [firstSession]] | ||
let store = TestStore(initialState: initialState) { | ||
Schedule() | ||
} withDependencies: { | ||
$0[FileClient.self].saveFavorites = { @Sendable in XCTAssertEqual($0, firstSessionFavorited) } | ||
} | ||
|
||
await store.send(.view(.favoriteIconTapped(firstSession))) | ||
await store.receive(\.savedFavorites) { | ||
$0.favorites = firstSessionFavorited | ||
} | ||
} | ||
|
||
@MainActor | ||
func testRemovingFavorites() async { | ||
let initialState: ScheduleFeature.Schedule.State = ScheduleTests.selectingDay1ScheduleWithOneFavorite | ||
let firstSession = initialState.day1!.schedules.first!.sessions.first! | ||
let noFavorites: Favorites = [initialState.day1!.title: []] | ||
let store = TestStore(initialState: initialState) { | ||
Schedule() | ||
} withDependencies: { | ||
$0[FileClient.self].saveFavorites = { @Sendable in XCTAssertEqual($0, noFavorites) } | ||
} | ||
|
||
await store.send(.view(.favoriteIconTapped(firstSession))) | ||
await store.receive(\.savedFavorites) { | ||
$0.favorites = noFavorites | ||
} | ||
} | ||
|
||
static let selectingDay1ScheduleWithNoFavorites = { | ||
var initialState = Schedule.State() | ||
initialState.selectedDay = .day1 | ||
initialState.day1 = .mock1 | ||
return initialState | ||
}() | ||
|
||
static let selectingDay1ScheduleWithOneFavorite = { | ||
var initialState = Schedule.State() | ||
initialState.selectedDay = .day1 | ||
initialState.day1 = .mock1 | ||
let firstSession = initialState.day1!.schedules.first!.sessions.first! | ||
initialState.favorites = [initialState.day1!.title: [firstSession]] | ||
return initialState | ||
}() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These logic should put in Reducer so that we can write test easily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry that I can't image which part of logic to put in Reducer.
Does it mean create new
Schedule.Action
case and handle it inReduce
?Or create computed property getting
Conference
inSchedule.State
?Now I implemented
Favorites
to useConference.title
as key for getting value of favorited[Session]
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved these logic to
Schedule.swift
in d3b9901.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this method if you have moved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for my lack of explanation.
I moved a part of logic as far as possible, so now
State.favorites: [String: [Session]]
is used in view whether each session is favorited or not, only.