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

검색 결과 -> 기억 상세 라우팅 추가 #102

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions InMyMemory/Presentation/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ let package = Package(
"BasePresentation",
"DesignKit",
"SearchInterface",
"MemoryDetailInterface",
.product(name: "CoreKit", package: "Shared"),
.product(name: "Entities", package: "Domain"),
.product(name: "Interfaces", package: "Domain"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import RxFlow
import CoreKit
import Interfaces
import MemoryDetailInterface

public final class SearchFlow: Flow {

Expand All @@ -35,6 +36,12 @@
case .searchIsComplete:
return .end(forwardToParentFlowWithStep: AppStep.searchIsComplete)

case .memoryDetailIsRequired(let memoryID):
return navigationToMemoryDetail(memoryID: memoryID)

case .memoryDetailIsComplete:
return popMemoryDetail()

Check warning on line 44 in InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift#L39-L44

Added lines #L39 - L44 were not covered by tests
default:
return .none
}
Expand All @@ -47,4 +54,21 @@
))
}

private func navigationToMemoryDetail(memoryID: UUID) -> FlowContributors {
let flow = injector.resolve(MemoryDetailBuildable.self).build(memoryID: memoryID, injector: injector)
Flows.use(flow, when: .created) { [weak self] root in
self?.rootViewController.navigationController?.pushViewController(root, animated: true)
}
return .one(flowContributor: .contribute(
withNextPresentable: flow,
withNextStepper: OneStepper(withSingleStep: AppStep.memoryDetailIsRequired(memoryID))
))
}

Check warning on line 66 in InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift#L57-L66

Added lines #L57 - L66 were not covered by tests

private func popMemoryDetail() -> FlowContributors {
rootViewController.navigationController?.popViewController(animated: true)
rootViewController.refresh()
return .none
}

Check warning on line 72 in InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Sources/SearchPresentation/SearchFlow.swift#L68-L72

Added lines #L68 - L72 were not covered by tests

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@
final class SearchReactor: Reactor, Stepper {

enum Action {
case search(String)
case search
case updateKeyword(String?)
case closeDidTap
case itemDidTap(IndexPath)
}

struct State {
var keyword: String?
var isLoading: Bool = false
var isEmpty: Bool = true
var sections: [SearchSection] = []
}

enum Mutation {
case setKeyword(String?)
case setLoading(Bool)
case setSections([Memory], [Emotion], [Todo])
}
Expand All @@ -53,26 +56,45 @@

func mutate(action: Action) -> Observable<Mutation> {
switch action {
case .search(let keyword):
case .search:
guard let keyword = currentState.keyword else { return .empty() }
return .concat([
.just(.setLoading(true)),
search(keyword: keyword),
.just(.setLoading(false))
])

case .updateKeyword(let keyword):
return .just(.setKeyword(keyword))

case .closeDidTap:
steps.accept(AppStep.searchIsComplete)
return .empty()

case .itemDidTap(let indexPath):
print(indexPath)
guard let item = currentState.sections[safe: indexPath.section]?.items[safe: indexPath.row] else {
return .empty()

Check warning on line 76 in InMyMemory/Presentation/Sources/SearchPresentation/SearchReactor.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Sources/SearchPresentation/SearchReactor.swift#L76

Added line #L76 was not covered by tests
}
switch item {
case .emotion(let model):
print("Emotion Did Tap")

case .memory(let model):
steps.accept(AppStep.memoryDetailIsRequired(model.id))

case .todo(let model):
print("Todo Did Tap")
}
return .empty()
}
}

func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .setKeyword(let keyword):
newState.keyword = keyword

case .setLoading(let isLoading):
newState.isLoading = isLoading

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,28 @@
}

override func bind(reactor: SearchReactor) {
bindAction(reactor)
bindState(reactor)
bindAction(reactor)
bindETC(reactor)
}

override func refresh() {
reactor?.action.onNext(.search)
}

Check warning on line 91 in InMyMemory/Presentation/Sources/SearchPresentation/SearchViewController.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Sources/SearchPresentation/SearchViewController.swift#L89-L91

Added lines #L89 - L91 were not covered by tests

private func bindAction(_ reactor: SearchReactor) {
navigationView.rx.leftButtonDidTap
.map { Reactor.Action.closeDidTap }
.bind(to: reactor.action)
.disposed(by: disposeBag)

searchInputView.rx.text
.map { Reactor.Action.updateKeyword($0) }
.bind(to: reactor.action)
.disposed(by: disposeBag)

searchInputView.rx.searchDidTap
.map { Reactor.Action.search($0) }
.map { _ in Reactor.Action.search }
.bind(to: reactor.action)
.disposed(by: disposeBag)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//
// SearchReactorTests.swift
//
//
// Created by 홍성준 on 1/23/24.
//

@testable import SearchPresentation
import PresentationTestSupport
import DomainTestSupport
import Entities
import Interfaces
import UseCases
import CoreKit
import BasePresentation
import XCTest
import Quick
import Nimble
import ReactorKit
import RxSwift
import RxRelay
import RxFlow

final class SearchReactorTests: QuickSpec {

override class func spec() {
var sut: SearchReactor!
var useCase: SearchUseCaseInterface!
var emotionRepository: EmotionRepositoryMock!
var memoryRepository: MemoryRepositoryMock!
var todoRepository: TodoRepositoryMock!
var stepBinder: StepBinder!
var disposeBag: DisposeBag!

describe("SearchReactor 테스트") {
beforeEach {
emotionRepository = .init()
memoryRepository = .init()
todoRepository = .init()
useCase = SearchUseCase(
emotionRepository: emotionRepository,
memoryRepository: memoryRepository,
todoRepository: todoRepository
)
stepBinder = .init()
disposeBag = .init()
sut = .init(useCase: useCase)
sut.steps
.bind(to: stepBinder.step)
.disposed(by: disposeBag)
}

context("updateKeyword가 호출되면") {
let keyword = "Test Keyword"
beforeEach {
sut.action.onNext(.updateKeyword(keyword))
}

it("Keyword 상태 값이 변경된다") {
expect(sut.currentState.keyword) == keyword
}
}

describe("nil인 문자가 입력되었을 때") {
beforeEach {
sut.action.onNext(.updateKeyword(nil))
}

context("검색을 누르면") {
beforeEach {
sut.action.onNext(.search)
}

it("검색을 호출하지 않는다") {
expect { memoryRepository.readKeywordCallCount } == 0
expect { emotionRepository.readKeywordCallCount } == 0
expect { todoRepository.readKeywordCallCount } == 0
}
}
}

describe("값이 있는 문자가 입력되었을 때") {
beforeEach {
sut.action.onNext(.updateKeyword("Test Keyword"))
}

context("검색을 누르면") {
beforeEach {
sut.action.onNext(.search)
}

it("검색을 호출한다") {
expect { memoryRepository.readKeywordCallCount } == 1
expect { emotionRepository.readKeywordCallCount } == 1
expect { todoRepository.readKeywordCallCount } == 1
}
}
}

describe("Section 테스트") {
let memoryID: UUID = .init()
let emotionID: UUID = .init()
let todoID: UUID = .init()
let memoryIndexPath: IndexPath = .init(row: 0, section: 0)
let emotionIndexPath: IndexPath = .init(row: 0, section: 1)
let todoIndexPath: IndexPath = .init(row: 0, section: 2)

beforeEach {
memoryRepository.readKeywordMemories = [.init(id: memoryID, images: [], note: "Memory", date: .init())]
emotionRepository.readKeywordEmotions = [.init(id: emotionID, note: "Emotion", emotionType: .good, date: .init())]
todoRepository.readKeywordTodos = [.init(id: todoID, note: "Todo", isCompleted: true, date: .init())]
sut.action.onNext(.updateKeyword("Test Keyword"))
sut.action.onNext(.search)
}

it("Section은 Memory, Emotion, Todo 순으로 저장된다") {
expect {
guard case .memory(let model) = sut.currentState.sections[memoryIndexPath.section].items[memoryIndexPath.row] else {
return .failed(reason: "올바르지 않은 데이터 타입")

Check warning on line 119 in InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift#L119

Added line #L119 was not covered by tests
}
return model.id == memoryID ? .succeeded : .failed(reason: "올바르지 않은 ID")
}.to(succeed())

expect {
guard case .emotion(let model) = sut.currentState.sections[emotionIndexPath.section].items[emotionIndexPath.row] else {
return .failed(reason: "올바르지 않은 데이터 타입")

Check warning on line 126 in InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift#L126

Added line #L126 was not covered by tests
}
return model.id == emotionID ? .succeeded : .failed(reason: "올바르지 않은 ID")
}.to(succeed())

expect {
guard case .todo(let model) = sut.currentState.sections[todoIndexPath.section].items[todoIndexPath.row] else {
return .failed(reason: "올바르지 않은 데이터 타입")

Check warning on line 133 in InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift#L133

Added line #L133 was not covered by tests
}
return model.id == todoID ? .succeeded : .failed(reason: "올바르지 않은 ID")
}.to(succeed())
}

context("Memory 아이템이 선택되면") {
beforeEach {
sut.action.onNext(.itemDidTap(memoryIndexPath))
}

it("memoryDetailIsRequired로 라우팅된다") {
let step = try unwrap(stepBinder.steps.last as? AppStep)
expect {
guard case .memoryDetailIsRequired(let id) = step else {
return .failed(reason: "올바르지 않은 라우팅")

Check warning on line 148 in InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift#L148

Added line #L148 was not covered by tests
}
return id == memoryID ? .succeeded : .failed(reason: "올바르지 않은 ID")
}.to(succeed())
}
}

context("Emotion 아이템이 선택되면") {
beforeEach {
stepBinder.steps = []
sut.action.onNext(.itemDidTap(emotionIndexPath))
}

it("라우팅이 호출되지 않는다") {
expect { stepBinder.steps.isEmpty } == true
}
}

context("Todo 아이템이 선택되면") {
beforeEach {
stepBinder.steps = []
sut.action.onNext(.itemDidTap(todoIndexPath))
}

it("라우팅이 호출되지 않는다") {
expect { stepBinder.steps.isEmpty } == true
}
}
}

context("closeDidTap을 호출하면") {
beforeEach {
sut.action.onNext(.closeDidTap)
}

it("searchIsComplete로 라우팅된다") {
let step = try unwrap(stepBinder.steps.last as? AppStep)
expect {
guard case .searchIsComplete = step else {
return .failed(reason: "올바르지 않은 라우팅")

Check warning on line 187 in InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift

View check run for this annotation

Codecov / codecov/patch

InMyMemory/Presentation/Tests/SearchPresentationTests/SearchReactorTests.swift#L187

Added line #L187 was not covered by tests
}
return .succeeded
}.to(succeed())
}
}
}
}

}