xfy d3befaabec 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>
2026-05-19 11:47:08 +08:00

180 lines
6.0 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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())
}
}