- 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>
180 lines
6.0 KiB
Kotlin
180 lines
6.0 KiB
Kotlin
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())
|
||
}
|
||
}
|