diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/learning_activities/domain/model/LearningActivity.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/learning_activities/domain/model/LearningActivity.kt index 193b638ed4..276af875d2 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/learning_activities/domain/model/LearningActivity.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/learning_activities/domain/model/LearningActivity.kt @@ -15,8 +15,6 @@ data class LearningActivity( private val targetTypeValue: String, @SerialName("type") private val typeValue: Int, - @SerialName("is_current") - val isCurrent: Boolean, @SerialName("title") val title: String = "", @SerialName("hypercoins_award") diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlanSection.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlanSection.kt index a2cf6a6430..567567850f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlanSection.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlanSection.kt @@ -15,6 +15,8 @@ data class StudyPlanSection( val targetId: Long? = null, @SerialName("target_type") val targetType: String? = null, + @SerialName("next_activity_id") + val nextActivityId: Long? = null, @SerialName("is_visible") val isVisible: Boolean, @SerialName("title") diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StateExtentions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StateExtentions.kt index 17f61010d2..ad80862a31 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StateExtentions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StateExtentions.kt @@ -1,19 +1,52 @@ package org.hyperskill.app.study_plan.widget.presentation import org.hyperskill.app.learning_activities.domain.model.LearningActivity +import org.hyperskill.app.learning_activities.domain.model.LearningActivityState import org.hyperskill.app.study_plan.domain.model.StudyPlanSection import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType -internal fun StudyPlanWidgetFeature.State.firstSection(): StudyPlanSection? = +/** + * Returns true if the section is supported in the study plan, when it is visible and its type is supported. + * @see StudyPlanSectionType + */ +internal val StudyPlanSection.isSupportedInStudyPlan: Boolean + get() = isVisible && StudyPlanSectionType.supportedTypes().contains(type) + +/** + * Finds first supported section in the study plan. + * + * @return first [StudyPlanSection] if it is supported in the study plan, otherwise null. + * @see StudyPlanSection.isSupportedInStudyPlan + */ +internal fun StudyPlanWidgetFeature.State.getCurrentSection(): StudyPlanSection? = studyPlanSections.values .firstOrNull { it.studyPlanSection.isSupportedInStudyPlan } ?.studyPlanSection +/** + * Finds current activity in the study plan. If the current section is root topics, then the next activity is returned. + * Otherwise, the first activity with [LearningActivityState.TODO] state is returned. + * + * @return current [LearningActivity]. + */ +internal fun StudyPlanWidgetFeature.State.getCurrentActivity(): LearningActivity? = + getCurrentSection()?.let { section -> + getSectionActivities(section.id) + .firstOrNull { + if (section.type == StudyPlanSectionType.ROOT_TOPICS) { + it.id == section.nextActivityId + } else { + it.state == LearningActivityState.TODO + } + } + } + +/** + * @param sectionId target section id. + * @return list of [LearningActivity] for the given section with [sectionId]. + */ internal fun StudyPlanWidgetFeature.State.getSectionActivities(sectionId: Long): List = studyPlanSections[sectionId] ?.studyPlanSection ?.activities ?.mapNotNull { id -> activities[id] } ?: emptyList() - -internal val StudyPlanSection.isSupportedInStudyPlan: Boolean - get() = isVisible && StudyPlanSectionType.supportedTypes().contains(type) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt index f4259d1d30..8376334ff1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt @@ -1,5 +1,7 @@ package org.hyperskill.app.study_plan.widget.presentation +import kotlin.math.max +import kotlin.math.min import org.hyperskill.app.learning_activities.domain.model.LearningActivityTargetType import org.hyperskill.app.learning_activities.domain.model.LearningActivityType import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder @@ -11,6 +13,7 @@ import org.hyperskill.app.study_plan.domain.analytic.StudyPlanClickedSectionHype import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalClickedGoToHomeScreenHyperskillAnalyticEvent import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalHiddenHyperskillAnalyticEvent import org.hyperskill.app.study_plan.domain.analytic.StudyPlanStageImplementUnsupportedModalShownHyperskillAnalyticEvent +import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType import org.hyperskill.app.study_plan.domain.model.StudyPlanStatus import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.Action import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.InternalAction @@ -22,6 +25,10 @@ import ru.nobird.app.presentation.redux.reducer.StateReducer internal typealias StudyPlanWidgetReducerResult = Pair> class StudyPlanWidgetReducer : StateReducer { + companion object { + const val SECTION_ROOT_TOPICS_PAGE_SIZE = 10 + } + override fun reduce(state: State, message: Message): StudyPlanWidgetReducerResult = when (message) { is Message.Initialize -> @@ -38,7 +45,7 @@ class StudyPlanWidgetReducer : StateReducer { state.copy( studyPlanSections = state.studyPlanSections.mapValues { (sectionId, sectionInfo) -> sectionInfo.copy( - contentStatus = if (sectionId == state.firstSection()?.id) { + contentStatus = if (sectionId == state.getCurrentSection()?.id) { sectionInfo.contentStatus } else { StudyPlanWidgetFeature.ContentStatus.IDLE @@ -209,9 +216,9 @@ class StudyPlanWidgetReducer : StateReducer { ) to setOf( InternalAction.FetchActivities( sectionId = message.sectionId, - activitiesIds = section.studyPlanSection.activities, + activitiesIds = getPaginatedActivitiesIds(section), sentryTransaction = HyperskillSentryTransactionBuilder.buildStudyPlanWidgetFetchLearningActivities( - isCurrentSection = message.sectionId == state.firstSection()?.id + isCurrentSection = message.sectionId == state.getCurrentSection()?.id ) ), InternalAction.LogAnalyticEvent( @@ -250,9 +257,9 @@ class StudyPlanWidgetReducer : StateReducer { updateSectionState(StudyPlanWidgetFeature.ContentStatus.LOADING) to setOfNotNull( InternalAction.FetchActivities( sectionId = sectionId, - activitiesIds = section.studyPlanSection.activities, + activitiesIds = getPaginatedActivitiesIds(section), sentryTransaction = HyperskillSentryTransactionBuilder.buildStudyPlanWidgetFetchLearningActivities( - isCurrentSection = sectionId == state.firstSection()?.id + isCurrentSection = sectionId == state.getCurrentSection()?.id ) ), logAnalyticEventAction @@ -270,6 +277,19 @@ class StudyPlanWidgetReducer : StateReducer { } } + internal fun getPaginatedActivitiesIds(section: StudyPlanWidgetFeature.StudyPlanSectionInfo): List = + if (section.studyPlanSection.type == StudyPlanSectionType.ROOT_TOPICS && + section.studyPlanSection.nextActivityId != null + ) { + val startIndex = + max(0, section.studyPlanSection.activities.indexOf(section.studyPlanSection.nextActivityId)) + val endIndex = + min(startIndex + (SECTION_ROOT_TOPICS_PAGE_SIZE - 1), section.studyPlanSection.activities.size - 1) + section.studyPlanSection.activities.slice(startIndex..endIndex) + } else { + section.studyPlanSection.activities + } + private fun handleActivityClicked(state: State, activityId: Long): StudyPlanWidgetReducerResult { val activity = state.activities[activityId] ?: return state to emptySet() @@ -282,7 +302,7 @@ class StudyPlanWidgetReducer : StateReducer { ) ) - if (!activity.isCurrent) { + if (activity.id != state.getCurrentActivity()?.id) { return state to setOf(logAnalyticEventAction) } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/view/StudyPlanWidgetViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/view/StudyPlanWidgetViewStateMapper.kt index 9f3e9a1484..4af0bcc7ea 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/view/StudyPlanWidgetViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/view/StudyPlanWidgetViewStateMapper.kt @@ -5,7 +5,8 @@ import org.hyperskill.app.core.view.mapper.DateFormatter import org.hyperskill.app.learning_activities.domain.model.LearningActivity import org.hyperskill.app.learning_activities.domain.model.LearningActivityState import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature -import org.hyperskill.app.study_plan.widget.presentation.firstSection +import org.hyperskill.app.study_plan.widget.presentation.getCurrentActivity +import org.hyperskill.app.study_plan.widget.presentation.getCurrentSection import org.hyperskill.app.study_plan.widget.presentation.getSectionActivities import org.hyperskill.app.study_plan.widget.view.StudyPlanWidgetViewState.SectionContent @@ -19,14 +20,15 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) { } private fun getLoadedWidgetContent(state: StudyPlanWidgetFeature.State): StudyPlanWidgetViewState.Content { - val firstSectionId = state.firstSection()?.id + val currentSectionId = state.getCurrentSection()?.id + val currentActivityId = state.getCurrentActivity()?.id return StudyPlanWidgetViewState.Content( sections = state.studyPlan?.sections?.mapNotNull { sectionId -> val sectionInfo = state.studyPlanSections[sectionId] ?: return@mapNotNull null val section = sectionInfo.studyPlanSection - val shouldShowSectionStatistics = firstSectionId == section.id || sectionInfo.isExpanded + val shouldShowSectionStatistics = currentSectionId == section.id || sectionInfo.isExpanded StudyPlanWidgetViewState.Section( id = section.id, @@ -34,7 +36,8 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) { subtitle = section.subtitle.takeIf { it.isNotEmpty() }, content = getSectionContent( state = state, - sectionInfo = sectionInfo + sectionInfo = sectionInfo, + currentActivityId = currentActivityId ), formattedTopicsCount = if (shouldShowSectionStatistics) { formatTopicsCount( @@ -59,7 +62,8 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) { private fun getSectionContent( sectionInfo: StudyPlanWidgetFeature.StudyPlanSectionInfo, - state: StudyPlanWidgetFeature.State + state: StudyPlanWidgetFeature.State, + currentActivityId: Long? ): SectionContent = if (sectionInfo.isExpanded) { when (sectionInfo.contentStatus) { @@ -69,14 +73,14 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) { if (activities.isEmpty()) { SectionContent.Loading } else { - getContent(activities) + getContent(activities, currentActivityId) } } StudyPlanWidgetFeature.ContentStatus.ERROR -> SectionContent.Error StudyPlanWidgetFeature.ContentStatus.LOADED -> { val activities = state.getSectionActivities(sectionInfo.studyPlanSection.id) if (activities.isNotEmpty()) { - getContent(activities) + getContent(activities, currentActivityId) } else { SectionContent.Error } @@ -86,14 +90,14 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: DateFormatter) { SectionContent.Collapsed } - private fun getContent(activities: List): SectionContent.Content = + private fun getContent(activities: List, currentActivityId: Long?): SectionContent.Content = SectionContent.Content( sectionItems = activities.map { activity -> StudyPlanWidgetViewState.SectionItem( id = activity.id, title = activity.title.ifBlank { activity.id.toString() }, state = when (activity.state) { - LearningActivityState.TODO -> if (activity.isCurrent) { + LearningActivityState.TODO -> if (activity.id == currentActivityId) { StudyPlanWidgetViewState.SectionItemState.NEXT } else { StudyPlanWidgetViewState.SectionItemState.LOCKED diff --git a/shared/src/commonTest/kotlin/org/hyperskill/StudyPlanWidgetTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/StudyPlanWidgetTest.kt index 5b0df831e9..a4bd665c6e 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/StudyPlanWidgetTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/StudyPlanWidgetTest.kt @@ -25,7 +25,7 @@ import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType import org.hyperskill.app.study_plan.domain.model.StudyPlanStatus import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetReducer -import org.hyperskill.app.study_plan.widget.presentation.firstSection +import org.hyperskill.app.study_plan.widget.presentation.getCurrentSection import org.hyperskill.app.study_plan.widget.view.StudyPlanWidgetViewState import org.hyperskill.app.study_plan.widget.view.StudyPlanWidgetViewStateMapper @@ -72,7 +72,7 @@ class StudyPlanWidgetTest { } ) - assertEquals(studyPlanSections.first { it.id == expectedSectionId }, state.firstSection()) + assertEquals(studyPlanSections.first { it.id == expectedSectionId }, state.getCurrentSection()) } @Test @@ -181,6 +181,16 @@ class StudyPlanWidgetTest { @Test fun `Loaded sections should be filtered by supportance`() { + assertEquals( + setOf( + StudyPlanSectionType.STAGE, + StudyPlanSectionType.EXTRA_TOPICS, + StudyPlanSectionType.ROOT_TOPICS + ), + StudyPlanSectionType.supportedTypes(), + "Test should be updated according to new supported types" + ) + val visibleUnsupportedSection = studyPlanSectionStub( id = 0, isVisible = true, @@ -407,7 +417,12 @@ class StudyPlanWidgetTest { sectionViewState( section = section.studyPlanSection, content = StudyPlanWidgetViewState.SectionContent.Content( - listOf(studyPlanSectionItemStub(activityId)) + listOf( + studyPlanSectionItemStub( + activityId, + state = StudyPlanWidgetViewState.SectionItemState.NEXT + ) + ) ) ) ) @@ -440,7 +455,12 @@ class StudyPlanWidgetTest { sectionViewState( section = section.studyPlanSection, content = StudyPlanWidgetViewState.SectionContent.Content( - listOf(studyPlanSectionItemStub(activityId)) + listOf( + studyPlanSectionItemStub( + activityId, + state = StudyPlanWidgetViewState.SectionItemState.NEXT + ) + ) ) ) ) @@ -493,7 +513,7 @@ class StudyPlanWidgetTest { studyPlanSectionItemStub( activityId = 0, title = "Activity 1", - state = StudyPlanWidgetViewState.SectionItemState.LOCKED + state = StudyPlanWidgetViewState.SectionItemState.NEXT ) ) ) @@ -531,7 +551,7 @@ class StudyPlanWidgetTest { studyPlanSectionItemStub( activityId = 0, title = "0", - state = StudyPlanWidgetViewState.SectionItemState.LOCKED + state = StudyPlanWidgetViewState.SectionItemState.NEXT ) ) ) @@ -641,7 +661,7 @@ class StudyPlanWidgetTest { ) ), activities = mapOf( - 0L to stubLearningActivity(id = 0, isCurrent = true), + 0L to stubLearningActivity(id = 0), 1L to stubLearningActivity(id = 1) ), sectionsStatus = StudyPlanWidgetFeature.ContentStatus.LOADED @@ -653,10 +673,24 @@ class StudyPlanWidgetTest { @Test fun `Click on non current learning activity should do nothing`() { - val activityId = 0L val state = StudyPlanWidgetFeature.State( - activities = mapOf(activityId to stubLearningActivity(activityId, isCurrent = false)) + studyPlan = studyPlanStub(id = 0, sections = listOf(0, 1)), + studyPlanSections = mapOf( + 0L to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = 0, activities = listOf(0)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ), + 1L to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = 1, activities = listOf(1)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), + activities = mapOf(0L to stubLearningActivity(id = 0), 1L to stubLearningActivity(id = 1)), + sectionsStatus = StudyPlanWidgetFeature.ContentStatus.LOADED ) + val activityId = 1L val (newState, actions) = reducer.reduce(state, StudyPlanWidgetFeature.Message.ActivityClicked(activityId)) @@ -671,12 +705,20 @@ class StudyPlanWidgetTest { val activityId = 0L val projectId = 1L val stageId = 2L + val sectionId = 3L + val state = StudyPlanWidgetFeature.State( - studyPlan = studyPlanStub(id = 0, projectId = projectId), + studyPlan = studyPlanStub(id = 0, projectId = projectId, sections = listOf(sectionId)), + studyPlanSections = mapOf( + sectionId to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = sectionId, activities = listOf(activityId)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), activities = mapOf( activityId to stubLearningActivity( activityId, - isCurrent = true, type = LearningActivityType.IMPLEMENT_STAGE, targetType = LearningActivityTargetType.STAGE, targetId = stageId @@ -698,11 +740,17 @@ class StudyPlanWidgetTest { fun `Click on stage implement learning activity with non stage target should do nothing`() { val activityId = 0L val state = StudyPlanWidgetFeature.State( - studyPlan = studyPlanStub(id = 0), + studyPlan = studyPlanStub(id = 0, sections = listOf(0)), + studyPlanSections = mapOf( + 0L to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = 0, activities = listOf(activityId)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), activities = mapOf( activityId to stubLearningActivity( activityId, - isCurrent = true, type = LearningActivityType.IMPLEMENT_STAGE, targetType = LearningActivityTargetType.STEP, targetId = 1L @@ -722,12 +770,19 @@ class StudyPlanWidgetTest { fun `Click on learn topic learning activity should navigate to step screen`() { val activityId = 0L val stepId = 1L + val sectionId = 2L val state = StudyPlanWidgetFeature.State( - studyPlan = studyPlanStub(id = 0), + studyPlan = studyPlanStub(id = 0, sections = listOf(sectionId)), + studyPlanSections = mapOf( + sectionId to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = sectionId, activities = listOf(activityId)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), activities = mapOf( activityId to stubLearningActivity( activityId, - isCurrent = true, type = LearningActivityType.LEARN_TOPIC, targetType = LearningActivityTargetType.STEP, targetId = stepId @@ -755,11 +810,17 @@ class StudyPlanWidgetTest { fun `Click on learn topic learning activity with non step target should do nothing`() { val activityId = 0L val state = StudyPlanWidgetFeature.State( - studyPlan = studyPlanStub(id = 0), + studyPlan = studyPlanStub(id = 0, sections = listOf(0)), + studyPlanSections = mapOf( + 0L to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = 0, activities = listOf(activityId)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), activities = mapOf( activityId to stubLearningActivity( activityId, - isCurrent = true, type = LearningActivityType.LEARN_TOPIC, targetType = LearningActivityTargetType.STAGE, targetId = 1L @@ -778,16 +839,23 @@ class StudyPlanWidgetTest { @Test fun `Click on implement stage activity with ide required should show unsupported modal`() { val activityId = 0L + val sectionId = 1L val state = StudyPlanWidgetFeature.State( studyPlan = studyPlanStub(id = 0, projectId = 1L), + studyPlanSections = mapOf( + sectionId to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub(id = sectionId, activities = listOf(activityId)), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), activities = mapOf( activityId to stubLearningActivity( activityId, - isCurrent = true, type = LearningActivityType.IMPLEMENT_STAGE, targetType = LearningActivityTargetType.STAGE, targetId = 1L, - isIdeRequired = true, + isIdeRequired = true ) ) ) @@ -842,6 +910,177 @@ class StudyPlanWidgetTest { } } + @Test + fun `Get paginated activities ids in non root topics section`() { + val expectedActivitiesIds = listOf(1L, 2L, 3L, 4L, 5L) + val section = StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = 0, + type = StudyPlanSectionType.STAGE, + activities = expectedActivitiesIds + ), + isExpanded = false, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + assertEquals(expectedActivitiesIds, reducer.getPaginatedActivitiesIds(section)) + } + + @Test + fun `Get paginated activities ids in empty root topics section`() { + val expectedActivitiesIds = emptyList() + val section = StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = 0, + type = StudyPlanSectionType.ROOT_TOPICS, + activities = expectedActivitiesIds + ), + isExpanded = false, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + assertEquals(expectedActivitiesIds, reducer.getPaginatedActivitiesIds(section)) + } + + @Test + fun `Get paginated activities ids in root topics section when end index greater than size`() { + val expectedActivitiesIds = listOf(3L, 4L, 5L) + val section = StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = 0, + type = StudyPlanSectionType.ROOT_TOPICS, + activities = (0L..5L).toList(), + nextActivityId = 3L + ), + isExpanded = false, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + assertEquals(expectedActivitiesIds, reducer.getPaginatedActivitiesIds(section)) + } + + @Test + fun `Get paginated activities ids in root topics section returns correct result page size`() { + val expectedActivitiesIds = (5L..14L).toList() + val section = StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = 0, + type = StudyPlanSectionType.ROOT_TOPICS, + activities = (0L..100L).toList(), + nextActivityId = 5L + ), + isExpanded = false, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + assertEquals(expectedActivitiesIds, reducer.getPaginatedActivitiesIds(section)) + } + + @Test + fun `Get paginated activities ids in root topics section when activities not contains next activity id`() { + val expectedActivitiesIds = (0L..4L).toList() + val section = StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = 0, + type = StudyPlanSectionType.ROOT_TOPICS, + activities = expectedActivitiesIds, + nextActivityId = 10L + ), + isExpanded = false, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + assertEquals(expectedActivitiesIds, reducer.getPaginatedActivitiesIds(section)) + } + + @Test + fun `Activity with id section next_activity_id in ViewState will be next for root topics section`() { + val nextActivityId = 0L + val notNextActivityId = 1L + val sectionId = 2L + val state = StudyPlanWidgetFeature.State( + studyPlan = studyPlanStub(id = 0, sections = listOf(sectionId)), + studyPlanSections = mapOf( + sectionId to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = sectionId, + activities = listOf(nextActivityId, notNextActivityId), + nextActivityId = nextActivityId + ), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), + activities = mapOf( + notNextActivityId to stubLearningActivity(notNextActivityId), + nextActivityId to stubLearningActivity(nextActivityId) + ), + sectionsStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + val viewState = studyPlanWidgetViewStateMapper.map(state) + + val viewSectionItems = ( + (viewState as? StudyPlanWidgetViewState.Content) + ?.sections + ?.firstOrNull() + ?.content as? StudyPlanWidgetViewState.SectionContent.Content + )?.sectionItems ?: fail("Unexpected view state: $viewState") + + assertEquals( + StudyPlanWidgetViewState.SectionItemState.NEXT, + viewSectionItems.first { it.id == nextActivityId }.state + ) + assertEquals( + StudyPlanWidgetViewState.SectionItemState.LOCKED, + viewSectionItems.first { it.id == notNextActivityId }.state + ) + } + + @Test + fun `First activity in ViewState will be next for not root topics section`() { + val firstActivityId = 0L + val secondActivityId = 1L + val sectionId = 2L + val state = StudyPlanWidgetFeature.State( + studyPlan = studyPlanStub(id = 0, sections = listOf(sectionId)), + studyPlanSections = mapOf( + sectionId to StudyPlanWidgetFeature.StudyPlanSectionInfo( + studyPlanSection = studyPlanSectionStub( + id = sectionId, + activities = listOf(firstActivityId, secondActivityId), + nextActivityId = null + ), + isExpanded = true, + contentStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + ), + activities = mapOf( + firstActivityId to stubLearningActivity(firstActivityId), + secondActivityId to stubLearningActivity(secondActivityId) + ), + sectionsStatus = StudyPlanWidgetFeature.ContentStatus.LOADED + ) + + val viewState = studyPlanWidgetViewStateMapper.map(state) + + val viewSectionItems = ( + (viewState as? StudyPlanWidgetViewState.Content) + ?.sections + ?.firstOrNull() + ?.content as? StudyPlanWidgetViewState.SectionContent.Content + )?.sectionItems ?: fail("Unexpected view state: $viewState") + + assertEquals( + StudyPlanWidgetViewState.SectionItemState.NEXT, + viewSectionItems.first { it.id == firstActivityId }.state + ) + assertEquals( + StudyPlanWidgetViewState.SectionItemState.LOCKED, + viewSectionItems.first { it.id == secondActivityId }.state + ) + } + private fun sectionViewState( section: StudyPlanSection, content: StudyPlanWidgetViewState.SectionContent, @@ -881,7 +1120,8 @@ class StudyPlanWidgetTest { topicsCount: Int = 0, completedTopicsCount: Int = 0, secondsToComplete: Float = 0f, - activities: List = emptyList() + activities: List = emptyList(), + nextActivityId: Long? = null ) = StudyPlanSection( id = id, @@ -889,6 +1129,7 @@ class StudyPlanWidgetTest { typeValue = type.value, targetId = 0, targetType = "", + nextActivityId = nextActivityId, isVisible = isVisible, title = "", subtitle = "", @@ -904,7 +1145,6 @@ class StudyPlanWidgetTest { targetId: Long = 0L, type: LearningActivityType = LearningActivityType.LEARN_TOPIC, targetType: LearningActivityTargetType = LearningActivityTargetType.STEP, - isCurrent: Boolean = false, title: String = "", isIdeRequired: Boolean = false ) = @@ -913,7 +1153,6 @@ class StudyPlanWidgetTest { stateValue = state.value, targetId = targetId, typeValue = type.value, - isCurrent = isCurrent, targetTypeValue = targetType.value, title = title, isIdeRequired = isIdeRequired