test: 新增 ShiftPattern、CalendarUtils、CalendarViewModel 单元测试
- ShiftPatternTest: 锚点前后、跨周期循环、负天数取模、空 cycle、 单元素 cycle、多样化周期、data class 属性 - CalendarUtilsExtraTest: calculateWeeksCountForPage(跨月/跨年)、 relativeDayDescription(今天/昨天/明天/N天前后/跨年月)、 formatLunarDate(农历前缀/正月初一/多日期验证) - CalendarViewModelStateTest: 初始状态、selectDate(含 currentMonth/Year 联动)、 increment/decrementYear、selectMonthFromYearView、shiftKindAt、 showLegalHoliday、onDrag/onExpandDrag progress 更新与 clamp、 getMonthDays 与 selectedDate/today 交互 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d58be45bc
commit
d3befaabec
@ -0,0 +1,365 @@
|
||||
package plus.rua.project
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
private class StateTestFixedClock(private val instant: Instant) : Clock {
|
||||
override fun now(): Instant = instant
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖 [CalendarViewModel] 中与日期选择、年视图、班次、拖拽 progress 等
|
||||
* 同步可观察状态相关的逻辑。
|
||||
*
|
||||
* 动画完成的最终状态(例如 [CalendarViewModel.isCollapsed] 在 spring
|
||||
* 动画结束后的取值)需要 MonotonicFrameClock 驱动,不在本测试集合范围内。
|
||||
*/
|
||||
class CalendarViewModelStateTest {
|
||||
|
||||
// 固定 today = 2026/5/15
|
||||
private val fixedInstant = Instant.parse("2026-05-15T00:00:00Z")
|
||||
private val testClock = StateTestFixedClock(fixedInstant)
|
||||
|
||||
private fun createViewModel(): CalendarViewModel {
|
||||
val scope = CoroutineScope(Dispatchers.Unconfined)
|
||||
return CalendarViewModel(coroutineScope = scope, clock = testClock)
|
||||
}
|
||||
|
||||
// ---- 初始状态 ----
|
||||
|
||||
@Test
|
||||
fun init_selectedDateIsToday() {
|
||||
val vm = createViewModel()
|
||||
assertEquals(LocalDate(2026, 5, 15), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_isCollapsedDefaultsFalse() {
|
||||
assertFalse(createViewModel().isCollapsed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_collapseProgressDefaultsZero() {
|
||||
assertEquals(0f, createViewModel().collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_isYearViewDefaultsFalse() {
|
||||
assertFalse(createViewModel().isYearView)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_yearViewProgressDefaultsZero() {
|
||||
assertEquals(0f, createViewModel().yearViewProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_yearViewYearDefaultsToTodayYear() {
|
||||
assertEquals(2026, createViewModel().yearViewYear)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_showLegalHolidayDefaultsFalse() {
|
||||
assertFalse(createViewModel().showLegalHoliday)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_shiftPatternHasDefault() {
|
||||
val pattern = createViewModel().shiftPattern
|
||||
assertNotNull(pattern)
|
||||
assertEquals(LocalDate(2026, 5, 15), pattern.anchorDate)
|
||||
assertEquals(4, pattern.cycle.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_currentMonthMatchesToday() {
|
||||
assertEquals(5, createViewModel().currentMonth)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun init_currentYearMatchesToday() {
|
||||
assertEquals(2026, createViewModel().currentYear)
|
||||
}
|
||||
|
||||
// ---- selectDate ----
|
||||
|
||||
@Test
|
||||
fun selectDate_updatesSelectedDate() {
|
||||
val vm = createViewModel()
|
||||
vm.selectDate(LocalDate(2026, 6, 1))
|
||||
assertEquals(LocalDate(2026, 6, 1), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectDate_currentMonthFollowsSelection() {
|
||||
val vm = createViewModel()
|
||||
vm.selectDate(LocalDate(2026, 8, 20))
|
||||
assertEquals(8, vm.currentMonth)
|
||||
assertEquals(2026, vm.currentYear)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectDate_yearFollowsSelection() {
|
||||
val vm = createViewModel()
|
||||
vm.selectDate(LocalDate(2027, 1, 1))
|
||||
assertEquals(2027, vm.currentYear)
|
||||
assertEquals(1, vm.currentMonth)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectDate_pastDate_updatesCorrectly() {
|
||||
val vm = createViewModel()
|
||||
vm.selectDate(LocalDate(2020, 12, 31))
|
||||
assertEquals(LocalDate(2020, 12, 31), vm.selectedDate)
|
||||
assertEquals(12, vm.currentMonth)
|
||||
assertEquals(2020, vm.currentYear)
|
||||
}
|
||||
|
||||
// ---- incrementYear / decrementYear ----
|
||||
|
||||
@Test
|
||||
fun incrementYear_increasesYearViewYear() {
|
||||
val vm = createViewModel()
|
||||
vm.incrementYear()
|
||||
assertEquals(2027, vm.yearViewYear)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decrementYear_decreasesYearViewYear() {
|
||||
val vm = createViewModel()
|
||||
vm.decrementYear()
|
||||
assertEquals(2025, vm.yearViewYear)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun incrementDecrementYear_consecutiveCalls() {
|
||||
val vm = createViewModel()
|
||||
repeat(5) { vm.incrementYear() }
|
||||
assertEquals(2031, vm.yearViewYear)
|
||||
repeat(3) { vm.decrementYear() }
|
||||
assertEquals(2028, vm.yearViewYear)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun incrementYear_doesNotAffectSelectedDate() {
|
||||
val vm = createViewModel()
|
||||
val before = vm.selectedDate
|
||||
vm.incrementYear()
|
||||
assertEquals(before, vm.selectedDate)
|
||||
}
|
||||
|
||||
// ---- selectMonthFromYearView ----
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_sameYearOtherMonth_setsFirstDayOfMonth() {
|
||||
val vm = createViewModel()
|
||||
vm.selectMonthFromYearView(8)
|
||||
assertEquals(LocalDate(2026, 8, 1), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_currentYearAndMonth_setsToToday() {
|
||||
val vm = createViewModel()
|
||||
// yearViewYear = 2026, today.month = 5
|
||||
vm.selectMonthFromYearView(5)
|
||||
assertEquals(LocalDate(2026, 5, 15), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_otherYear_setsFirstDay() {
|
||||
val vm = createViewModel()
|
||||
vm.incrementYear() // yearViewYear = 2027
|
||||
vm.selectMonthFromYearView(5)
|
||||
assertEquals(LocalDate(2027, 5, 1), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_setsIsYearViewFalse() {
|
||||
val vm = createViewModel()
|
||||
vm.selectMonthFromYearView(3)
|
||||
assertFalse(vm.isYearView)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_january() {
|
||||
val vm = createViewModel()
|
||||
vm.selectMonthFromYearView(1)
|
||||
assertEquals(LocalDate(2026, 1, 1), vm.selectedDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectMonthFromYearView_december() {
|
||||
val vm = createViewModel()
|
||||
vm.selectMonthFromYearView(12)
|
||||
assertEquals(LocalDate(2026, 12, 1), vm.selectedDate)
|
||||
}
|
||||
|
||||
// ---- shiftKindAt ----
|
||||
|
||||
@Test
|
||||
fun shiftKindAt_anchorDate_returnsWork() {
|
||||
// default pattern: anchor 2026-05-15, cycle WORK/WORK/OFF/OFF
|
||||
val vm = createViewModel()
|
||||
assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 5, 15)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftKindAt_dayAfterAnchor_returnsWork() {
|
||||
val vm = createViewModel()
|
||||
assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 5, 16)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftKindAt_twoDaysAfterAnchor_returnsOff() {
|
||||
val vm = createViewModel()
|
||||
assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 5, 17)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftKindAt_nullPattern_returnsNull() {
|
||||
val vm = createViewModel()
|
||||
vm.shiftPattern = null
|
||||
assertNull(vm.shiftKindAt(LocalDate(2026, 5, 15)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftKindAt_customPattern_usesNewPattern() {
|
||||
val vm = createViewModel()
|
||||
vm.shiftPattern = ShiftPattern(
|
||||
anchorDate = LocalDate(2026, 5, 15),
|
||||
cycle = listOf(ShiftKind.OFF, ShiftKind.WORK)
|
||||
)
|
||||
assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 5, 15)))
|
||||
assertEquals(ShiftKind.WORK, vm.shiftKindAt(LocalDate(2026, 5, 16)))
|
||||
assertEquals(ShiftKind.OFF, vm.shiftKindAt(LocalDate(2026, 5, 17)))
|
||||
}
|
||||
|
||||
// ---- showLegalHoliday ----
|
||||
|
||||
@Test
|
||||
fun showLegalHoliday_canBeToggled() {
|
||||
val vm = createViewModel()
|
||||
assertFalse(vm.showLegalHoliday)
|
||||
vm.showLegalHoliday = true
|
||||
assertTrue(vm.showLegalHoliday)
|
||||
vm.showLegalHoliday = false
|
||||
assertFalse(vm.showLegalHoliday)
|
||||
}
|
||||
|
||||
// ---- onDrag: 折叠拖拽(同步路径:snapTo)----
|
||||
|
||||
@Test
|
||||
fun onDrag_positiveDelta_increasesProgress() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(0.3f)
|
||||
assertEquals(0.3f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDrag_accumulatesAcrossCalls() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(0.2f)
|
||||
vm.onDrag(0.3f)
|
||||
assertEquals(0.5f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDrag_clampsAtOne() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(0.8f)
|
||||
vm.onDrag(0.8f)
|
||||
assertEquals(1f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDrag_clampsAtZeroWhenNegativeFromZero() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(-0.3f)
|
||||
assertEquals(0f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onDrag_negativeAfterPositive_canDecrease() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(0.5f)
|
||||
vm.onDrag(-0.2f)
|
||||
assertEquals(0.3f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
// ---- onExpandDrag: 展开拖拽 ----
|
||||
|
||||
@Test
|
||||
fun onExpandDrag_updatesProgress() {
|
||||
val vm = createViewModel()
|
||||
// 先把 progress 推到 1
|
||||
vm.onDrag(1f)
|
||||
assertEquals(1f, vm.collapseProgress, 0.001f)
|
||||
// 展开方向:delta 为负
|
||||
vm.onExpandDrag(-0.4f)
|
||||
assertEquals(0.6f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onExpandDrag_clampsAtZero() {
|
||||
val vm = createViewModel()
|
||||
vm.onDrag(0.5f)
|
||||
vm.onExpandDrag(-1f)
|
||||
assertEquals(0f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onExpandDrag_clampsAtOne() {
|
||||
val vm = createViewModel()
|
||||
vm.onExpandDrag(2f)
|
||||
assertEquals(1f, vm.collapseProgress, 0.001f)
|
||||
}
|
||||
|
||||
// ---- getMonthDays 与 selectedDate 配合 ----
|
||||
|
||||
@Test
|
||||
fun getMonthDays_updatesIsSelectedAfterSelectDate() {
|
||||
val vm = createViewModel()
|
||||
vm.selectDate(LocalDate(2026, 5, 20))
|
||||
val days = vm.getMonthDays(2026, 5)
|
||||
val selectedCell = days.first { it.isSelected }
|
||||
assertEquals(20, selectedCell.date.day)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMonthDays_noCellSelectedInOtherMonth() {
|
||||
val vm = createViewModel()
|
||||
// selectedDate 默认是今天(5/15),不在 2026/8 月内(含跨月填充也不可能)
|
||||
val days = vm.getMonthDays(2026, 8)
|
||||
assertTrue(days.none { it.isSelected })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMonthDays_todayCellAlwaysReflectsTodayClock() {
|
||||
val vm = createViewModel()
|
||||
// 即便选中其他日期,isToday 依然根据 clock 注入的 today
|
||||
vm.selectDate(LocalDate(2026, 5, 20))
|
||||
val days = vm.getMonthDays(2026, 5)
|
||||
val todayCell = days.first { it.isToday }
|
||||
assertEquals(15, todayCell.date.day)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMonthDays_returnsMultipleOfSeven() {
|
||||
val vm = createViewModel()
|
||||
// 任何月份,cells 数都应该是 7 的倍数
|
||||
for (month in 1..12) {
|
||||
val size = vm.getMonthDays(2026, month).size
|
||||
assertEquals(0, size % 7, "Month 2026/$month size=$size not multiple of 7")
|
||||
assertTrue(size in 28..42, "Month 2026/$month size=$size out of [28, 42]")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package plus.rua.project
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ShiftPatternTest {
|
||||
|
||||
private val anchor = LocalDate(2026, 5, 15)
|
||||
private val twoOnTwoOff = ShiftPattern(
|
||||
anchorDate = anchor,
|
||||
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF, ShiftKind.OFF)
|
||||
)
|
||||
|
||||
// ---- kindAt: 锚点与同周期内 ----
|
||||
|
||||
@Test
|
||||
fun kindAt_anchorDate_returnsFirstInCycle() {
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(anchor))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_oneAfterAnchor_returnsSecondInCycle() {
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 5, 16)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_twoAfterAnchor_returnsThirdInCycle() {
|
||||
assertEquals(ShiftKind.OFF, twoOnTwoOff.kindAt(LocalDate(2026, 5, 17)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_threeAfterAnchor_returnsFourthInCycle() {
|
||||
assertEquals(ShiftKind.OFF, twoOnTwoOff.kindAt(LocalDate(2026, 5, 18)))
|
||||
}
|
||||
|
||||
// ---- kindAt: 周期循环 ----
|
||||
|
||||
@Test
|
||||
fun kindAt_fourAfterAnchor_wrapsToCycleStart() {
|
||||
// (5/19 - 5/15) % 4 = 0
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 5, 19)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_eightAfterAnchor_wrapsTwice() {
|
||||
// (5/23 - 5/15) % 4 = 0
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 5, 23)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_oneCycleLater_idx2_returnsOff() {
|
||||
// (5/21 - 5/15) % 4 = 2
|
||||
assertEquals(ShiftKind.OFF, twoOnTwoOff.kindAt(LocalDate(2026, 5, 21)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_manyCyclesLater_correctlyWraps() {
|
||||
// 100天后: (100) % 4 = 0
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 8, 23)))
|
||||
}
|
||||
|
||||
// ---- kindAt: 锚点之前的日期(负差值处理)----
|
||||
|
||||
@Test
|
||||
fun kindAt_oneDayBeforeAnchor_returnsLastInCycle() {
|
||||
// -1 mod 4 = 3 -> OFF (cycle[3])
|
||||
assertEquals(ShiftKind.OFF, twoOnTwoOff.kindAt(LocalDate(2026, 5, 14)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_twoDaysBeforeAnchor_returnsThirdInCycle() {
|
||||
// -2 mod 4 = 2 -> OFF (cycle[2])
|
||||
assertEquals(ShiftKind.OFF, twoOnTwoOff.kindAt(LocalDate(2026, 5, 13)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_threeDaysBeforeAnchor_returnsSecondInCycle() {
|
||||
// -3 mod 4 = 1 -> WORK (cycle[1])
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 5, 12)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_fourDaysBeforeAnchor_returnsFirstInCycle() {
|
||||
// -4 mod 4 = 0 -> WORK (cycle[0])
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 5, 11)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_manyDaysBeforeAnchor_correctlyWraps() {
|
||||
// -100 mod 4 = 0 -> WORK
|
||||
assertEquals(ShiftKind.WORK, twoOnTwoOff.kindAt(LocalDate(2026, 2, 4)))
|
||||
}
|
||||
|
||||
// ---- kindAt: 边界情况 ----
|
||||
|
||||
@Test
|
||||
fun kindAt_emptyCycle_returnsNull() {
|
||||
val pattern = ShiftPattern(anchorDate = anchor, cycle = emptyList())
|
||||
assertNull(pattern.kindAt(anchor))
|
||||
assertNull(pattern.kindAt(LocalDate(2026, 5, 16)))
|
||||
assertNull(pattern.kindAt(LocalDate(2026, 5, 14)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_singleElementCycle_alwaysReturnsThatElement() {
|
||||
val pattern = ShiftPattern(anchorDate = anchor, cycle = listOf(ShiftKind.WORK))
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(anchor))
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 20)))
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 1, 1)))
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2027, 12, 31)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_singleOffCycle_alwaysReturnsOff() {
|
||||
val pattern = ShiftPattern(anchorDate = anchor, cycle = listOf(ShiftKind.OFF))
|
||||
assertEquals(ShiftKind.OFF, pattern.kindAt(anchor))
|
||||
assertEquals(ShiftKind.OFF, pattern.kindAt(LocalDate(2030, 6, 15)))
|
||||
}
|
||||
|
||||
// ---- kindAt: 多样化周期 ----
|
||||
|
||||
@Test
|
||||
fun kindAt_threeOnOneOffCycle() {
|
||||
// 4 day cycle: WORK WORK WORK OFF
|
||||
val pattern = ShiftPattern(
|
||||
anchorDate = anchor,
|
||||
cycle = listOf(ShiftKind.WORK, ShiftKind.WORK, ShiftKind.WORK, ShiftKind.OFF)
|
||||
)
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 15))) // 0
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 16))) // 1
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 17))) // 2
|
||||
assertEquals(ShiftKind.OFF, pattern.kindAt(LocalDate(2026, 5, 18))) // 3
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 19))) // 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindAt_weekCycle_returnsCorrectDay() {
|
||||
// 7天周期:4天上班3天休息
|
||||
val pattern = ShiftPattern(
|
||||
anchorDate = anchor,
|
||||
cycle = listOf(
|
||||
ShiftKind.WORK, ShiftKind.WORK, ShiftKind.WORK, ShiftKind.WORK,
|
||||
ShiftKind.OFF, ShiftKind.OFF, ShiftKind.OFF
|
||||
)
|
||||
)
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 18))) // idx 3
|
||||
assertEquals(ShiftKind.OFF, pattern.kindAt(LocalDate(2026, 5, 19))) // idx 4
|
||||
assertEquals(ShiftKind.OFF, pattern.kindAt(LocalDate(2026, 5, 21))) // idx 6
|
||||
assertEquals(ShiftKind.WORK, pattern.kindAt(LocalDate(2026, 5, 22))) // idx 0 (next cycle)
|
||||
}
|
||||
|
||||
// ---- ShiftPattern: 元数据 ----
|
||||
|
||||
@Test
|
||||
fun shiftPattern_defaultNameIsChinese() {
|
||||
val pattern = ShiftPattern(anchorDate = anchor, cycle = listOf(ShiftKind.WORK))
|
||||
assertEquals("默认", pattern.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftPattern_customNameIsPreserved() {
|
||||
val pattern = ShiftPattern(
|
||||
anchorDate = anchor,
|
||||
cycle = listOf(ShiftKind.WORK),
|
||||
name = "夜班"
|
||||
)
|
||||
assertEquals("夜班", pattern.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun shiftPattern_dataClassEquality() {
|
||||
val a = ShiftPattern(anchor, listOf(ShiftKind.WORK, ShiftKind.OFF))
|
||||
val b = ShiftPattern(anchor, listOf(ShiftKind.WORK, ShiftKind.OFF))
|
||||
assertEquals(a, b)
|
||||
assertEquals(a.hashCode(), b.hashCode())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
package plus.rua.project.ui
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* 测试 CalendarUtils 中尚未被 [CalendarUtilsTest] 覆盖的函数:
|
||||
* - [calculateWeeksCountForPage]
|
||||
* - [relativeDayDescription]
|
||||
* - [formatLunarDate]
|
||||
*/
|
||||
class CalendarUtilsExtraTest {
|
||||
|
||||
// ---- calculateWeeksCountForPage ----
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_centerPage_returnsCurrentMonthRows() {
|
||||
// today = 2026/5/15 (May), May 2026 has 5 rows
|
||||
val today = LocalDate(2026, 5, 15)
|
||||
assertEquals(calculateWeeksCount(2026, 5), calculateWeeksCountForPage(START_PAGE, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_forwardOnePage_returnsNextMonthRows() {
|
||||
// From May 2026, +1 -> June 2026
|
||||
val today = LocalDate(2026, 5, 15)
|
||||
assertEquals(calculateWeeksCount(2026, 6), calculateWeeksCountForPage(START_PAGE + 1, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_backwardOnePage_returnsPreviousMonthRows() {
|
||||
// From May 2026, -1 -> April 2026
|
||||
val today = LocalDate(2026, 5, 15)
|
||||
assertEquals(calculateWeeksCount(2026, 4), calculateWeeksCountForPage(START_PAGE - 1, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_crossYearForward() {
|
||||
// From December 2026, +1 -> January 2027
|
||||
val today = LocalDate(2026, 12, 10)
|
||||
assertEquals(calculateWeeksCount(2027, 1), calculateWeeksCountForPage(START_PAGE + 1, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_crossYearBackward() {
|
||||
// From January 2026, -1 -> December 2025
|
||||
val today = LocalDate(2026, 1, 10)
|
||||
assertEquals(calculateWeeksCount(2025, 12), calculateWeeksCountForPage(START_PAGE - 1, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calculateWeeksCountForPage_twelvePagesForward_returnsSameMonthOfNextYear() {
|
||||
val today = LocalDate(2026, 5, 15)
|
||||
// +12 -> May 2027
|
||||
assertEquals(calculateWeeksCount(2027, 5), calculateWeeksCountForPage(START_PAGE + 12, today))
|
||||
}
|
||||
|
||||
// ---- relativeDayDescription ----
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_today_returnsToday() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
assertEquals("今天", relativeDayDescription(today, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_yesterday_returnsYesterday() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
val yesterday = LocalDate(2026, 5, 18)
|
||||
assertEquals("昨天", relativeDayDescription(yesterday, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_tomorrow_returnsTomorrow() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
val tomorrow = LocalDate(2026, 5, 20)
|
||||
assertEquals("明天", relativeDayDescription(tomorrow, today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_twoDaysBefore_returnsXDaysAgo() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
assertEquals("2天前", relativeDayDescription(LocalDate(2026, 5, 17), today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_twoDaysAfter_returnsXDaysLater() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
assertEquals("2天后", relativeDayDescription(LocalDate(2026, 5, 21), today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_aWeekBefore_returnsCorrectDays() {
|
||||
val today = LocalDate(2026, 5, 19)
|
||||
assertEquals("7天前", relativeDayDescription(LocalDate(2026, 5, 12), today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_thirtyDaysAfter_returnsCorrectDays() {
|
||||
val today = LocalDate(2026, 5, 1)
|
||||
assertEquals("30天后", relativeDayDescription(LocalDate(2026, 5, 31), today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_crossMonthBackward_returnsCorrectDays() {
|
||||
val today = LocalDate(2026, 5, 2)
|
||||
assertEquals("3天前", relativeDayDescription(LocalDate(2026, 4, 29), today))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun relativeDayDescription_crossYearForward_returnsCorrectDays() {
|
||||
val today = LocalDate(2025, 12, 30)
|
||||
assertEquals("5天后", relativeDayDescription(LocalDate(2026, 1, 4), today))
|
||||
}
|
||||
|
||||
// ---- formatLunarDate ----
|
||||
|
||||
@Test
|
||||
fun formatLunarDate_startsWithLunarPrefix() {
|
||||
val result = formatLunarDate(LocalDate(2026, 5, 19))
|
||||
assertTrue(result.startsWith("农历"), "Expected to start with '农历', got: $result")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatLunarDate_january1_2026_returnsCorrectLunar() {
|
||||
// 2026/1/1 公历 -> 2025年农历十一月十二
|
||||
val result = formatLunarDate(LocalDate(2026, 1, 1))
|
||||
assertTrue(result.startsWith("农历"), "Expected '农历' prefix, got: $result")
|
||||
// 验证不是空字符串
|
||||
assertTrue(result.length > 2, "Lunar date description should contain month and day")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatLunarDate_lunarNewYear2026_returnsFirstDayOfFirstMonth() {
|
||||
// 2026年农历正月初一 = 2026/2/17 公历
|
||||
val result = formatLunarDate(LocalDate(2026, 2, 17))
|
||||
assertEquals("农历正月初一", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatLunarDate_anyDate_containsMonthAndDayNames() {
|
||||
// 仅验证格式:农历 + 月 + 日
|
||||
for (day in listOf(
|
||||
LocalDate(2026, 3, 1),
|
||||
LocalDate(2026, 6, 30),
|
||||
LocalDate(2026, 12, 25)
|
||||
)) {
|
||||
val result = formatLunarDate(day)
|
||||
assertTrue(result.startsWith("农历"), "Expected '农历' prefix for $day, got: $result")
|
||||
assertTrue(result.length >= 5, "Result for $day too short: $result")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user