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

[둘리] 3, 4 단계 영화 티켓 예매 제출합니다. #47

Merged
merged 47 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
afc8cca
test: PriceCalculatorTest 생성
hyemdooly Apr 18, 2023
4ca2988
test: TicketTest 수정
hyemdooly Apr 18, 2023
aaace58
refactor: fold를 사용하여 코드 단축
hyemdooly Apr 18, 2023
067af6b
refactor: PlayingDateTimes로 클래스명과 코드 수정
hyemdooly Apr 18, 2023
f5c4730
refactor: package 이동으로 인한 수정
hyemdooly Apr 18, 2023
eeaeb22
refactor: 필요없는 주석 삭제
hyemdooly Apr 18, 2023
d8f91d8
refactor: TicketResultActivity View 세팅 코드 분리
hyemdooly Apr 18, 2023
417aca7
feat: SeatSelectActivity xml 작성
hyemdooly Apr 20, 2023
d9672f2
refactor: DateSpinnerListener 삭제, findViewById 한번만 하도록 수정
hyemdooly Apr 20, 2023
e174e3d
refactor: MovieListItemListener 분리
hyemdooly Apr 20, 2023
debeaca
fix: onItemClickListener -> SelectedListener 수정
hyemdooly Apr 20, 2023
76c0a90
feat: SeatView 구현
hyemdooly Apr 20, 2023
3738630
refactor: Ticket Model 구조 변경
hyemdooly Apr 20, 2023
e892f4b
feat: 좌석 선택 페이지로 넘어가도록 수정
hyemdooly Apr 20, 2023
2c8cc14
feat: 좌석 Grade 클래스 추가
hyemdooly Apr 20, 2023
21c892b
feat: TheaterInfo 생성
hyemdooly Apr 21, 2023
61f7022
feat: SeatSelectSystem 생성
hyemdooly Apr 21, 2023
09e0766
feat: SelectResult Success sead class 생성
hyemdooly Apr 21, 2023
cf52062
refactor: PriceCalculator 리팩토링
hyemdooly Apr 21, 2023
8291de2
feat: PriceSystem 생성
hyemdooly Apr 21, 2023
f4d6e7c
refactor: JUnit4 버전 수정
hyemdooly Apr 21, 2023
cd9c49e
feat: Seat 클래스 생성, seats custom getter 생성
hyemdooly Apr 21, 2023
e351227
refactor: 패키지명 수정
hyemdooly Apr 21, 2023
7ff59f2
feat: 좌석 선택 액티비티 구현
hyemdooly Apr 21, 2023
55df55c
feat: Listener 수정, 자리 선택 액티비티 구현 완료
hyemdooly Apr 21, 2023
025733a
refactor: Ticket 액티비티에서 자리 순서대로 출력하도록 수정
hyemdooly Apr 21, 2023
61c6a5f
refactor: count max값 추가
hyemdooly Apr 21, 2023
29d7245
refactor: PriceModel 구현 및 수정
hyemdooly Apr 22, 2023
a410609
feat: Dialog 구현
hyemdooly Apr 22, 2023
1e294bc
feat: Dialog NegativeButton Listener 추가
hyemdooly Apr 22, 2023
371ec72
refactor: UI 변경 사항 수정
hyemdooly Apr 22, 2023
9c41ac5
test: MovieDetailActivity, SeatSelectActivity UI Test 추가
hyemdooly Apr 22, 2023
21c60b1
test: TicketResultActivity UI Test 추가
hyemdooly Apr 22, 2023
04b076e
feat: ListView -> RecyclerView로 변경
hyemdooly Apr 22, 2023
74fbd10
feat: RecyclerView Ad View 추가
hyemdooly Apr 22, 2023
cc1abcc
refactor: ViewHolder property private 수정
hyemdooly Apr 22, 2023
3b89b78
test: MovieListActivityTest 추가
hyemdooly Apr 22, 2023
ea36840
docs: Update README.md
hyemdooly Apr 22, 2023
aa5050c
refactor: MovieDetailActivityTest 수정
hyemdooly Apr 24, 2023
8a3b1df
refactor: MovieDetailActivityTest 수정
hyemdooly Apr 24, 2023
55c309b
refactor: Listener interface 삭제
hyemdooly Apr 24, 2023
27c24b4
refactor: layoutManager xml에서 지정, View에서 Bundle이 아닌 Data 주입
hyemdooly Apr 24, 2023
03d294e
refactor: ReserveInfoModel 추가 및 수정
hyemdooly Apr 24, 2023
bb03b6a
refactor: sealed class view holder, view type enum class 생성, item들 xm…
hyemdooly Apr 24, 2023
0522cc9
Merge remote-tracking branch 'origin/step4' into step4
hyemdooly Apr 24, 2023
8f35ee7
refactor: 코드 변경에 따른 테스트 수정, 패키지 변경
hyemdooly Apr 24, 2023
9f09fcd
refactor: Log 삭제
hyemdooly Apr 24, 2023
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
38 changes: 24 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,33 @@

## 액티비티
- MovieListActivity
- TicketingActivity
- MovieTicketActivity
- MovieDetailActivity
- SeatSelectActivity
- TicketResultActivity

## 어댑터
- MoviesAdapter
- MovieListAdapter

## 데이터
## 모델
- Movie
- 이미지, 제목, 상영일(PlayingTimes), 러닝타임, 소개
- TicketingInfo
- 영화 이름, 상영일, 몇명, 가격, 무슨 결제
- PlayingDateTimes
- Ticket
- 영화 이름, 상영일, 몇명, 가격
- DiscountPolicy (다형성을 위한 추상 클래스)
- MorningPolicy (조조)
- MovieDayPolicy (무비데이)
- NightPolicy (야간)
- Price
- 음수 체크
- 티켓 한 장의 가격은 13000원
- 영화 가격에 할인을 적용한다.
- Discount 구현 클래스를 리스트로 받는다.
- PlayingTimes
- 날짜-상영시간 map 타입으로 가지고 있다.
- Discount (인터페이스)
- calculate 메소드
- Grade (enum class)
- price (등급별 가격)
- Seat
- row, col (범위 0 이상)
- TheaterInfo
- 등급별 가격, 좌석 정보

## 서비스
- PriceSystem (SelectResult에 따른 가격 변화 계산기)
- PriceCalculator (정책에 따른 가격 계산기)
- SeatSelectSystem
- SelectResult (결과를 나타내는 sealed class)
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.3.0")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.5.1")
implementation(project(":domain"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package woowacourse.movie

import android.content.Intent
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Test
import woowacourse.movie.activity.moviedetail.MovieDetailActivity
import woowacourse.movie.domain.movie.Movie
import woowacourse.movie.model.MovieModel
import woowacourse.movie.model.toPresentation
import java.time.LocalDate

class MovieDetailActivityTest {

private val movieModel = Movie.of(
"해리포터와 마법사의 돌 1",
LocalDate.of(2023, 3, 1),
LocalDate.of(2023, 3, 31),
152,
"해리포터 첫 번째 시리즈"
).toPresentation(R.drawable.img)
Comment on lines +20 to +26

Choose a reason for hiding this comment

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

Movie가 변경되면 이 테스트 코드 크래시가 나겠네요 :)
Movie class와는 거의 상관 없는 이 테스트가 문제가 생기면 꽤나 당황스러울 게 예상되네요 🙂

테스트에는 정말 필요한 의존 관계만 갖게 작성해보세요 :)


lateinit var activityScenario: ActivityScenario<MovieDetailActivity>

@Test
fun 영화_정보를_띄운다() {
startActivity(movieModel)
onView(withId(R.id.text_title)).check(matches(withText("해리포터와 마법사의 돌 1")))
onView(withId(R.id.text_playing_date)).check(matches(withText("상영일: 2023.3.1 ~ 2023.3.31")))
onView(withId(R.id.text_description)).check(matches(withText("해리포터 첫 번째 시리즈")))
}

@Test
fun 플러스_버튼을_클릭하면_매수가_올라간다() {
startActivity(movieModel)
onView(withId(R.id.btn_plus)).perform(click())
onView(withId(R.id.text_count)).check(matches(withText("2")))
}

@Test
fun 마이너스_버튼을_클릭하면_매수가_내려간다() {
startActivity(movieModel)
onView(withId(R.id.btn_plus)).perform(click())
onView(withId(R.id.btn_plus)).perform(click())
onView(withId(R.id.btn_minus)).perform(click())
onView(withId(R.id.text_count)).check(matches(withText("2")))
}

private fun startActivity(movie: MovieModel) {
val intent = Intent(ApplicationProvider.getApplicationContext(), MovieDetailActivity::class.java)
intent.putExtra(MovieDetailActivity.MOVIE_KEY, movieModel)
activityScenario = ActivityScenario.launch(intent)
}
}
Comment on lines +54 to +59

Choose a reason for hiding this comment

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

JUnit4에서 @Before는 파라미터를 받을 수 없고, 안드로이드에서는 JUnit4를 사용해야하는데 다른 방법이 있을까요?!

Before를 통해 클래스 필드를 공통적으로 선언해주고,
특정 테스트 메서드에서만 해당 필드를 필요한 데이터로 변경해줄 수 있겠지요 :)

123 changes: 123 additions & 0 deletions app/src/androidTest/java/woowacourse/movie/MovieListActivityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package woowacourse.movie

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import woowacourse.movie.activity.moviedetail.MovieDetailActivity
import woowacourse.movie.activity.movielist.CustomViewHolder
import woowacourse.movie.activity.movielist.ListViewType
import woowacourse.movie.activity.movielist.MovieListActivity
import woowacourse.movie.model.toPresentation
import woowacourse.movie.util.DummyData
import java.time.format.DateTimeFormatter

class MovieListActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(MovieListActivity::class.java)

val dummyDataToPresentation = DummyData.movies.map { it.toPresentation(R.drawable.img) }

@Before
fun setup() {
Intents.init()
}

@After
fun tearDown() {
Intents.release()
}

@Test
fun 아이템_3개마다_광고뷰를_띄운다() {
activityRule.scenario.onActivity { activity ->
val recyclerView = activity.findViewById<RecyclerView>(R.id.recycler_view)

val expected = recyclerView.adapter?.getItemViewType(3)
val actual = ListViewType.AD_VIEWTYPE.ordinal

assertEquals(expected, actual)
}
}

@Test
fun 광고를_제외한_모든_아이템은_영화_정보_뷰를_띄운다() {
activityRule.scenario.onActivity { activity ->
val recyclerView = activity.findViewById<RecyclerView>(R.id.recycler_view)

for (position in 0 until 3) {
val expected = recyclerView.adapter?.getItemViewType(position)
val actual = ListViewType.NORMAL_VIEWTYPE.ordinal

assertEquals(expected, actual)
}
}
}

@Test
fun 광고는_이미지를_띄운다() {
onView(withId(R.id.recycler_view))
.perform(scrollToPosition<CustomViewHolder>(3))
.check(matches(hasDescendant(withId(R.id.img_ad))))
}

@Test
fun 광고_이외_아이템은_영화_정보를_띄운다() {
val movie = dummyDataToPresentation[0]
val formatter = DateTimeFormatter.ofPattern("YYYY.M.d")
val dateTime =
"상영일: ${formatter.format(movie.startDate)} ~ ${formatter.format(movie.endDate)}"
val runningTime = "러닝타임: ${movie.runningTime}분"
onView(withId(R.id.recycler_view))
.perform(scrollToPosition<CustomViewHolder>(0))
.check(matches(hasDescendant(withText(movie.title))))
onView(withId(R.id.recycler_view)).check(matches(hasDescendant(withText(dateTime))))
onView(withId(R.id.recycler_view)).check(matches(hasDescendant(withText(runningTime))))
}

@Test
fun 예매_버튼을_누르면_상세_액티비티를_띄운다() {
onView(withId(R.id.recycler_view))
.perform(
actionOnItemAtPosition<CustomViewHolder>(
0,
clickChildViewWithId(R.id.btn_reserve),
),
)
intended(hasComponent(MovieDetailActivity::class.java.name))
}

fun clickChildViewWithId(id: Int): ViewAction? {
return object : ViewAction {
override fun getConstraints(): Matcher<View>? {
return null
}

override fun getDescription(): String {
return "Click on a child view with specified id."
}

override fun perform(uiController: UiController?, view: View) {
val v: View = view.findViewById(id)
v.performClick()
}
}
}
}
122 changes: 122 additions & 0 deletions app/src/androidTest/java/woowacourse/movie/SeatSelectActivityTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package woowacourse.movie

import android.content.Intent
import android.graphics.drawable.ColorDrawable
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.internal.util.Checks
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.junit.Rule
import org.junit.Test
import woowacourse.movie.activity.seatselect.SeatSelectActivity
import woowacourse.movie.model.ReserveInfoModel
import java.time.LocalDateTime

class SeatSelectActivityTest {
private val intent =
Intent(ApplicationProvider.getApplicationContext(), SeatSelectActivity::class.java).apply {
putExtra(
SeatSelectActivity.INFO_KEY,
ReserveInfoModel("해리포터와 마법사의 돌 1", LocalDateTime.of(2023, 3, 12, 12, 0, 0), 20),
)
}

@get:Rule
val activityRule = ActivityScenarioRule<SeatSelectActivity>(intent)

private val ids = listOf(
R.id.seat_a1, R.id.seat_a2, R.id.seat_a3, R.id.seat_a4,
R.id.seat_b1, R.id.seat_b2, R.id.seat_b3, R.id.seat_b4,
R.id.seat_c1, R.id.seat_c2, R.id.seat_c3, R.id.seat_c4,
R.id.seat_d1, R.id.seat_d2, R.id.seat_d3, R.id.seat_d4,
R.id.seat_e1, R.id.seat_e2, R.id.seat_e3, R.id.seat_e4,
)

@Test
fun 영화_제목을_띄운다() {
onView(withId(R.id.text_title))
.check(matches(withText("해리포터와 마법사의 돌 1")))
}

@Test
fun 아직_선택하지_않은_좌석을_클릭하면_색깔이_바뀐다() {
ids.forEach {
onView(withId(it)).perform(ViewActions.click())
onView(withId(it)).check(matches(isSelected()))
onView(withId(it)).check(matches(withBackgroundColor(R.color.select_seat)))
}
}

@Test
fun 이미_선택한_좌석을_클릭하면_원래_색깔로_바뀐다() {
ids.forEach {
onView(withId(it)).perform(ViewActions.click())
}
ids.forEach {
onView(withId(it)).perform(ViewActions.click())
onView(withId(it)).check(matches(not(isSelected())))
onView(withId(it)).check(matches(withBackgroundColor(R.color.white)))
}
}

@Test
fun 예매할_모든_좌석을_선택한_경우_버튼이_활성화된다() {
ids.forEach {
onView(withId(it)).perform(ViewActions.click())
}
onView(withId(R.id.btn_next)).check(matches(isEnabled()))
}

@Test
fun 예매할_모든_좌석을_선택하기_전까지_버튼이_활성화되지_않는다() {
ids.drop(1).forEach {
onView(withId(it)).perform(ViewActions.click())
}
onView(withId(R.id.btn_next)).check(matches(isNotEnabled()))
}

@Test
fun 좌석을_클릭하면_가격이_올라간다() {
ids.forEach {
onView(withId(it)).perform(ViewActions.click())
}
onView(withId(R.id.text_price)).check(matches(withText("248,000원")))
}

@Test
fun 좌석_선택을_해제하면_가격이_내려간다() {
onView(withId(R.id.seat_a1)).perform(ViewActions.click())
onView(withId(R.id.seat_a1)).perform(ViewActions.click())
onView(withId(R.id.text_price)).check(matches(withText("0원")))
}

private fun withBackgroundColor(@ColorInt color: Int): Matcher<View> {
Checks.checkNotNull(color)
return object : BoundedMatcher<View, TextView>(TextView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("TextView background color : $color")
}

override fun matchesSafely(item: TextView?): Boolean {
val backgroundColor = item?.background as ColorDrawable
val colorDrawable = ColorDrawable(ContextCompat.getColor(item.context, color))
return colorDrawable.color == backgroundColor.color
}
}
}
}
Loading