refactor(onair): 라이브 입장 흐름을 공통화한다

This commit is contained in:
2026-07-15 21:27:44 +09:00
parent cc23992d2e
commit 0b3042d29e
4 changed files with 318 additions and 197 deletions

View File

@@ -9,32 +9,16 @@ import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService
import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.common.Constants
import kr.co.vividnext.sodalive.common.LoadingDialog
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
import kr.co.vividnext.sodalive.common.ToastMessage
import kr.co.vividnext.sodalive.databinding.ActivityHomeOnAirLiveBinding
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.live.room.LiveRoomActivity
import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog
import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog
import kr.co.vividnext.sodalive.settings.language.LanguageManager
import kr.co.vividnext.sodalive.settings.language.LocaleHelper
import kr.co.vividnext.sodalive.v2.access.AccessRequirement
import kr.co.vividnext.sodalive.v2.access.ensureV2Access
import kr.co.vividnext.sodalive.v2.live.action.LiveActionCoordinator
import kr.co.vividnext.sodalive.v2.live.onair.model.HomeOnAirLivePageUiState
import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom
import kr.co.vividnext.sodalive.v2.live.onair.ui.HomeOnAirLiveAdapter
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
@UnstableApi
class HomeOnAirLiveActivity : BaseActivity<ActivityHomeOnAirLiveBinding>(
@@ -42,6 +26,16 @@ class HomeOnAirLiveActivity : BaseActivity<ActivityHomeOnAirLiveBinding>(
) {
private val viewModel: HomeOnAirLiveViewModel by viewModel()
private val liveViewModel: LiveViewModel by inject()
private val liveActionCoordinator: LiveActionCoordinator by lazy {
LiveActionCoordinator(
activity = this,
layoutInflater = layoutInflater,
fragmentManager = supportFragmentManager,
liveViewModel = liveViewModel,
screenWidthProvider = { screenWidth },
refreshHome = {}
)
}
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) }
private val adapter = HomeOnAirLiveAdapter { enterLiveRoom(it.roomId) }
private var isPageLoading = false
@@ -110,112 +104,9 @@ class HomeOnAirLiveActivity : BaseActivity<ActivityHomeOnAirLiveBinding>(
}
private fun enterLiveRoom(roomId: Long) {
ensureV2Access(AccessRequirement.Login) {
liveViewModel.getRoomDetail(roomId) { roomDetail ->
if (!canEnterHomeOnAirLiveRoom(roomDetail)) {
liveActionCoordinator.enterLiveRoom(roomId) {
Toast.makeText(applicationContext, R.string.common_error_unknown, Toast.LENGTH_LONG).show()
return@getRoomDetail
}
ensureV2Access(AccessRequirement.forAdultContent(roomDetail.isAdult)) {
enterLiveRoom(roomId, roomDetail)
}
}
}
}
private fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse) {
startService(
Intent(applicationContext, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.STOP.name
}
)
startService(
Intent(applicationContext, AudioContentPlayerService::class.java).apply {
action = "STOP_SERVICE"
}
)
val onEnterRoomSuccess = {
runOnUiThread {
startActivity(
Intent(applicationContext, LiveRoomActivity::class.java).apply {
putExtra(Constants.EXTRA_ROOM_ID, roomId)
}
)
}
}
if (roomDetail.manager.id == SharedPreferenceManager.userId) {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
} else if (roomDetail.price == 0 || roomDetail.isPaid) {
if (roomDetail.isPrivateRoom) {
showPasswordDialog(roomId, can = 0, onEnterRoomSuccess = onEnterRoomSuccess)
} else {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
}
} else {
showPaidLiveEntryDialog(
roomId = roomId,
beginDateTimeUtc = roomDetail.beginDateTimeUtc,
price = roomDetail.price,
isPrivateRoom = roomDetail.isPrivateRoom,
onEnterRoomSuccess = onEnterRoomSuccess
)
}
}
private fun showPasswordDialog(roomId: Long, can: Int, onEnterRoomSuccess: () -> Unit) {
LiveRoomPasswordDialog(
activity = this,
layoutInflater = layoutInflater,
can = can,
confirmButtonClick = { password ->
liveViewModel.enterRoom(
roomId = roomId,
onSuccess = onEnterRoomSuccess,
password = password
)
}
).show(screenWidth)
}
private fun showPaidLiveEntryDialog(
roomId: Long,
beginDateTimeUtc: String,
price: Int,
isPrivateRoom: Boolean,
onEnterRoomSuccess: () -> Unit
) {
if (isPrivateRoom) {
showPasswordDialog(roomId, can = price, onEnterRoomSuccess = onEnterRoomSuccess)
return
}
val locale = Locale(LanguageManager.getEffectiveLanguage(this))
val wrappedContext = LocaleHelper.wrap(this)
val beginDate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.parse(beginDateTimeUtc) ?: return
val now = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", locale)
val diffTime = now.time - beginDate.time
val hours = (diffTime / (1000 * 60 * 60)).toInt()
val mins = (diffTime / (1000 * 60)).toInt() % 60
LivePaymentDialog(
activity = this,
layoutInflater = layoutInflater,
title = wrappedContext.getString(R.string.live_paid_title),
startDateTime = if (hours >= 1) dateFormat.format(beginDate) else null,
nowDateTime = if (hours >= 1) dateFormat.format(now) else null,
desc = wrappedContext.getString(R.string.live_paid_desc, price),
desc2 = if (hours >= 1) wrappedContext.getString(R.string.live_paid_warning, hours, mins) else null,
confirmButtonTitle = wrappedContext.getString(R.string.live_paid_confirm),
confirmButtonClick = { liveViewModel.enterRoom(roomId, onEnterRoomSuccess) },
cancelButtonTitle = wrappedContext.getString(R.string.cancel),
cancelButtonClick = {}
).show(screenWidth)
}
private fun showToast(toastMessage: ToastMessage) {

View File

@@ -1,7 +0,0 @@
package kr.co.vividnext.sodalive.v2.live.onair.model
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
fun canEnterHomeOnAirLiveRoom(roomDetail: GetRoomDetailResponse): Boolean {
return roomDetail.channelName.isNullOrBlank().not()
}

View File

@@ -0,0 +1,297 @@
package kr.co.vividnext.sodalive.v2.live.action
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
class LiveActionCoordinatorSourceTest {
@Test
fun `Live coordinator는 공통 정책과 기존 레거시 동작을 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
assertTrue(source.contains("class LiveActionCoordinator("))
assertTrue(source.contains("LiveEntryPolicy.decide("))
assertTrue(source.contains("AccessRequirement.Login"))
assertTrue(source.contains("val requiresAdultAccess ="))
assertTrue(source.contains("requiresAdultContentAccess == true || roomDetail.isAdult"))
assertTrue(source.contains("AccessRequirement.forAdultContent(requiresAdultAccess)"))
assertTrue(source.contains("AudioContentPlayService.MusicAction.STOP.name"))
assertTrue(source.contains("AudioContentPlayerService::class.java"))
assertTrue(source.contains("LiveRoomPasswordDialog("))
assertTrue(source.contains("LivePaymentDialog("))
assertTrue(source.contains("LiveRoomDetailFragment("))
assertTrue(source.contains("LiveRoomActivity::class.java"))
assertFalse(source.contains("kr.co.vividnext.sodalive.v2.creator"))
}
@Test
fun `Live coordinator 기본 lifecycle predicate는 종료된 Activity 결과 처리를 막는다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
assertTrue(
source.contains(
"private val canHandleResult: () -> Boolean = " +
"{ !activity.isFinishing && !activity.isDestroyed }"
)
)
}
@Test
fun `Live coordinator는 입장 성공 callback 직전에 lifecycle을 다시 검사한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val entrySource = sourceSection(
source,
"fun enterLiveRoom(",
"private fun stopAudioPlayback()"
)
val successSource = sourceSection(
entrySource,
"val onEnterRoomSuccess = {",
"handleLiveEntry(roomId, roomDetail, decision, onEnterRoomSuccess)"
)
assertTrue(successSource.contains("activity.runOnUiThread"))
assertTrue(successSource.contains("runLiveActionUiSideEffect(::canHandleUiResult)"))
assertBefore(
successSource,
"runLiveActionUiSideEffect(::canHandleUiResult)",
"openLiveRoom(roomId)"
)
}
@Test
fun `Live coordinator는 시작 성공 navigation 직전에도 lifecycle을 다시 검사한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val startSource = sourceSection(
source,
"private fun startLive(roomId: Long)",
"private fun cancelLive(roomId: Long)"
)
assertTrue(startSource.contains("activity.runOnUiThread"))
assertBefore(
startSource,
"runLiveActionUiSideEffect(::canHandleUiResult)",
"openLiveRoom(roomId)"
)
}
@Test
fun `Live coordinator는 Activity stateSaved 호출 화면 predicate를 결합해 UI 결과를 검사한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val predicateSource = sourceSection(
source,
"private fun canHandleUiResult(): Boolean",
"private companion object"
)
assertTrue(predicateSource.contains("isActivityFinishing = activity.isFinishing"))
assertTrue(predicateSource.contains("isActivityDestroyed = activity.isDestroyed"))
assertTrue(predicateSource.contains("isStateSaved = fragmentManager.isStateSaved"))
assertTrue(predicateSource.contains("canHandleResult = canHandleResult"))
}
@Test
fun `Live coordinator는 안정적인 tag와 state로 상세 중복 표시를 막는다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val detailSource = sourceSection(
source,
"fun showLiveRoomDetail(roomId: Long)",
"fun enterLiveRoom("
)
assertTrue(source.contains("LIVE_ROOM_DETAIL_TAG"))
assertTrue(detailSource.contains("fragmentManager.findFragmentByTag(LIVE_ROOM_DETAIL_TAG)"))
assertTrue(detailSource.contains("canShowLiveRoomDetail("))
assertTrue(detailSource.contains("detailFragment.showNow(fragmentManager, LIVE_ROOM_DETAIL_TAG)"))
assertBefore(detailSource, "canShowLiveRoomDetail(", "LiveRoomDetailFragment(")
assertBefore(
detailSource,
"fragmentManager.findFragmentByTag(LIVE_ROOM_DETAIL_TAG)",
"LiveRoomDetailFragment("
)
assertFalse(detailSource.contains("detailFragment.isAdded"))
assertFalse(detailSource.contains("detailFragment.tag"))
}
@Test
fun `Live coordinator 공개 상세 진입점은 로그인과 성인 접근을 확인한 뒤 표시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val publicDetailSource = sourceSection(
source,
"fun showLiveRoomDetail(roomId: Long)",
"private fun showLiveRoomDetailContent(roomId: Long)"
)
assertTrue(publicDetailSource.contains("AccessRequirement.Login"))
assertTrue(publicDetailSource.contains("liveViewModel.getRoomDetail(roomId)"))
assertTrue(publicDetailSource.contains("AccessRequirement.forAdultContent(roomDetail.isAdult)"))
assertTrue(publicDetailSource.contains("showLiveRoomDetailContent(roomId)"))
assertBefore(publicDetailSource, "AccessRequirement.Login", "liveViewModel.getRoomDetail(roomId)")
assertBefore(
publicDetailSource,
"AccessRequirement.forAdultContent(roomDetail.isAdult)",
"showLiveRoomDetailContent(roomId)"
)
}
@Test
fun `Home Creator Channel On-air는 동일한 Live coordinator를 사용한다`() {
val home = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val creator = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val onAir = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt"
).readText()
assertTrue(home.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
assertTrue(home.contains("liveActionCoordinator.enterLiveRoom(item.roomId)"))
assertFalse(home.contains("liveViewModel.getRoomDetail(item.roomId)"))
assertTrue(creator.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
assertTrue(
creator.contains(
"liveActionCoordinator.enterLiveRoom(live.liveId, requiresAdultContentAccess = live.isAdult)"
)
)
assertTrue(onAir.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
assertTrue(onAir.contains("liveActionCoordinator.enterLiveRoom(roomId"))
assertFalse(onAir.contains("private fun enterLiveRoom(roomId: Long, roomDetail:"))
assertFalse(onAir.contains("private fun showPaidLiveEntryDialog("))
}
@Test
fun `Creator Channel은 라이브 생성 결과를 단일 handler에서 처리한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val handlerStartIndex = source.indexOf(
"private fun handleLiveCreationResult(result: LiveCreationResult)"
)
val handlerEndIndex = source.indexOf(
"override val shouldApplySystemBarTopInset",
handlerStartIndex
)
assertTrue(source.contains("resolveLiveCreationResult("))
assertTrue(handlerStartIndex >= 0)
assertTrue(handlerEndIndex > handlerStartIndex)
val handlerSource = source.substring(handlerStartIndex, handlerEndIndex)
assertTrue(handlerSource.contains("LiveCreationResult.Ignored -> Unit"))
assertTrue(
handlerSource.contains(
"LiveCreationResult.RefreshOnly -> homeActionDelegate?.refreshHome()"
)
)
assertTrue(handlerSource.contains("LiveCreationResult.Created ->"))
assertTrue(handlerSource.contains("is LiveCreationResult.Enter ->"))
}
@Test
fun `Live coordinator는 unavailable을 adult guard와 오디오 중단보다 먼저 처리한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val flowSource = sourceSection(
source,
"runLiveEntryDecisionFlow(",
"val loadRoomDetail ="
)
val unavailableIndex = flowSource.indexOf(
"handleUnavailableLive(roomId, requiresAdultAccess, onUnavailable)"
)
val adultGuardIndex = flowSource.indexOf(
"AccessRequirement.forAdultContent(requiresAdultAccess)"
)
val stopAudioIndex = flowSource.indexOf("stopAudioPlayback()")
assertTrue(unavailableIndex >= 0)
assertTrue(adultGuardIndex >= 0)
assertTrue(stopAudioIndex >= 0)
assertTrue(unavailableIndex < adultGuardIndex)
assertTrue(adultGuardIndex < stopAudioIndex)
assertFalse(source.contains("AccessRequirement.forAdultContent(requiresAdultContentAccess),"))
}
@Test
fun `Live coordinator는 unavailable 상세 fallback만 adult guard 뒤에 표시한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/action/LiveActionCoordinator.kt"
).readText()
val effectiveAdultAccess = "requiresAdultContentAccess == true || roomDetail.isAdult"
val fallbackStartIndex = source.indexOf("private fun handleUnavailableLive(")
val fallbackEndIndex = source.indexOf("private fun reservationRoom(", fallbackStartIndex)
assertTrue(source.contains("val requiresAdultAccess = $effectiveAdultAccess"))
assertTrue(source.indexOf(effectiveAdultAccess) == source.lastIndexOf(effectiveAdultAccess))
assertTrue(
source.contains("handleUnavailableLive(roomId, requiresAdultAccess, onUnavailable)")
)
assertTrue(fallbackStartIndex >= 0)
assertTrue(fallbackEndIndex > fallbackStartIndex)
val fallbackSource = source.substring(fallbackStartIndex, fallbackEndIndex)
val fallbackGuardIndex = fallbackSource.indexOf(
"AccessRequirement.forAdultContent(requiresAdultAccess)"
)
val callbackIndex = fallbackSource.indexOf("onUnavailable()")
val callbackReturnIndex = fallbackSource.indexOf("return", callbackIndex)
val showDetailIndex = fallbackSource.indexOf("showLiveRoomDetailContent(roomId)")
assertTrue(fallbackSource.contains("requiresAdultAccess: Boolean"))
assertTrue(fallbackSource.contains("if (onUnavailable != null)"))
assertFalse(fallbackSource.contains("roomDetail.isAdult"))
assertTrue(callbackIndex >= 0)
assertTrue(callbackReturnIndex >= 0)
assertTrue(fallbackGuardIndex >= 0)
assertTrue(showDetailIndex >= 0)
assertTrue(callbackIndex < callbackReturnIndex)
assertTrue(callbackReturnIndex < fallbackGuardIndex)
assertTrue(fallbackGuardIndex < showDetailIndex)
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }
?: error("Project file not found: $relativePath")
}
private fun sourceSection(source: String, startMarker: String, endMarker: String): String {
val startIndex = source.indexOf(startMarker)
assertTrue("Missing start marker: $startMarker", startIndex >= 0)
val endIndex = source.indexOf(endMarker, startIndex + startMarker.length)
assertTrue("Missing end marker: $endMarker", endIndex > startIndex)
return source.substring(startIndex, endIndex)
}
private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) {
val beforeIndex = source.indexOf(expectedBefore)
val afterIndex = source.indexOf(expectedAfter)
assertTrue("Missing expected source: $expectedBefore", beforeIndex >= 0)
assertTrue("Missing expected source: $expectedAfter", afterIndex >= 0)
assertTrue("Expected '$expectedBefore' before '$expectedAfter'", beforeIndex < afterIndex)
}
}

View File

@@ -1,9 +1,5 @@
package kr.co.vividnext.sodalive.v2.live.onair
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse
import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -12,72 +8,24 @@ import java.io.File
class HomeOnAirLiveEntryPolicyTest {
@Test
fun `channelName이 있으면 입장 분기를 허용한다`() {
assertTrue(canEnterHomeOnAirLiveRoom(roomDetail(channelName = "channel-a")))
}
@Test
fun `channelName이 null이면 입장 분기를 중단한다`() {
assertFalse(canEnterHomeOnAirLiveRoom(roomDetail(channelName = null)))
}
@Test
fun `channelName이 blank이면 입장 분기를 중단한다`() {
assertFalse(canEnterHomeOnAirLiveRoom(roomDetail(channelName = " ")))
}
@Test
fun `라이브 입장은 로그인 상세 조회 성인 가드 기존 입장 순서를 유지한다`() {
fun `On-air 라이브 입장은 공통 Live Action과 unavailable Toast를 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt"
).readText()
val entrySource = sourceSection(
source = source,
startMarker = "private fun enterLiveRoom(roomId: Long) {",
endMarker = "private fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse)"
endMarker = "private fun showToast(toastMessage: ToastMessage)"
)
val loginGuard = "ensureV2Access(AccessRequirement.Login)"
val roomDetailQuery = "liveViewModel.getRoomDetail(roomId) { roomDetail ->"
val adultGuard = "ensureV2Access(AccessRequirement.forAdultContent(roomDetail.isAdult))"
val enterRoom = "enterLiveRoom(roomId, roomDetail)"
assertBefore(entrySource, loginGuard, roomDetailQuery)
assertBefore(entrySource, roomDetailQuery, adultGuard)
assertBefore(entrySource, adultGuard, enterRoom)
assertFalse(source.contains("private fun ensureLoginAndAdultAuth"))
assertTrue(source.contains("private val liveActionCoordinator: LiveActionCoordinator by lazy"))
assertTrue(entrySource.contains("liveActionCoordinator.enterLiveRoom(roomId)"))
assertTrue(entrySource.contains("R.string.common_error_unknown"))
assertFalse(source.contains("private fun enterLiveRoom(roomId: Long, roomDetail:"))
assertFalse(source.contains("private fun showPaidLiveEntryDialog("))
assertFalse(source.contains("private fun showPasswordDialog("))
}
private fun roomDetail(channelName: String?) = GetRoomDetailResponse(
roomId = 1L,
price = 0,
title = "title",
notice = "notice",
isPaid = false,
isAdult = false,
genderRestriction = GenderRestriction.ALL,
isPrivateRoom = false,
password = null,
tags = emptyList(),
channelName = channelName,
beginDateTimeUtc = "2026-06-26T12:00:00",
isNotification = false,
numberOfParticipants = 1,
numberOfParticipantsTotal = 10,
manager = GetRoomDetailManager(
id = 100L,
nickname = "creator",
introduce = "",
youtubeUrl = null,
instagramUrl = null,
kakaoOpenChatUrl = null,
fancimmUrl = null,
xUrl = null,
profileImageUrl = "",
isCreator = true
),
participatingUsers = emptyList()
)
private fun sourceSection(source: String, startMarker: String, endMarker: String): String {
val start = source.indexOf(startMarker)
val end = source.indexOf(endMarker, start)
@@ -88,14 +36,6 @@ class HomeOnAirLiveEntryPolicyTest {
return source.substring(start, end)
}
private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) {
val beforeIndex = source.indexOf(expectedBefore)
val afterIndex = source.indexOf(expectedAfter, beforeIndex)
assertTrue("missing source: $expectedBefore", beforeIndex >= 0)
assertTrue("missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex)
}
private fun projectFile(relativePath: String): File {
val candidates = listOf(File(relativePath), File("../$relativePath"))
return candidates.firstOrNull { it.exists() }