diff --git a/app/src/test/java/com/xiaoqu/watch/data/AttendanceStatusTest.kt b/app/src/test/java/com/xiaoqu/watch/data/AttendanceStatusTest.kt new file mode 100644 index 0000000..5ae9f4b --- /dev/null +++ b/app/src/test/java/com/xiaoqu/watch/data/AttendanceStatusTest.kt @@ -0,0 +1,43 @@ +package com.xiaoqu.watch.data + +import com.google.gson.Gson +import com.xiaoqu.watch.data.punch.AttendanceStatus +import org.junit.Assert.* +import org.junit.Test + +/** + * AttendanceStatus API 契约测试 + * 用实际 API 返回的 JSON 验证 Gson 解析是否正确 + */ +class AttendanceStatusTest { + + private val gson = Gson() + + @Test + fun `parse actual API response - on duty`() { + // 实际 API 返回(2026-04-29 设备验证) + val json = """{"code":0,"data":{"onPunchState":1,"actualOnTime":"09:07","offPunchState":0,"actualOffTime":"","workAtStatus":0,"workStatus":0}}""" + val response = gson.fromJson(json, ApiResponseWrapper::class.java) + val data = gson.fromJson(gson.toJson(response.data), AttendanceStatus::class.java) + + assertEquals(1, data.onPunchState) + assertEquals("09:07", data.actualOnTime) + assertEquals(0, data.offPunchState) + assertEquals("", data.actualOffTime) + assertEquals(0, data.workStatus) + assertEquals(0, data.workAtStatus) + } + + @Test + fun `parse default values when fields missing`() { + val json = """{}""" + val data = gson.fromJson(json, AttendanceStatus::class.java) + assertEquals(0, data.onPunchState) + assertEquals(0, data.offPunchState) + assertNull(data.actualOnTime) + assertNull(data.actualOffTime) + } + + /** 辅助类:模拟 ApiResponse 外层结构 */ + data class ApiResponseWrapper(val code: Int = 0, val data: Any? = null) +} diff --git a/app/src/test/java/com/xiaoqu/watch/data/PunchButtonMatrixTest.kt b/app/src/test/java/com/xiaoqu/watch/data/PunchButtonMatrixTest.kt new file mode 100644 index 0000000..3905f6a --- /dev/null +++ b/app/src/test/java/com/xiaoqu/watch/data/PunchButtonMatrixTest.kt @@ -0,0 +1,51 @@ +package com.xiaoqu.watch.data + +import com.xiaoqu.watch.ui.punch.PunchUiState +import org.junit.Assert.* +import org.junit.Test + +/** + * 考勤打卡按钮显示矩阵测试 + * 验证 onPunchState × offPunchState → 按钮组合 + * + * 规则(源码分析 + 独立评审修正): + * - onPunchState=0 → 显示"上班打卡" + * - onPunchState=1, offPunchState=0 → 显示"下班打卡"(无撤销) + * - onPunchState=1, offPunchState=1 → 显示"撤销" + "下班打卡" + */ +class PunchButtonMatrixTest { + + @Test + fun `not punched in - should show punch in button only`() { + val state = PunchUiState(onPunchState = 0, offPunchState = 0) + assertTrue(shouldShowPunchIn(state)) + assertFalse(shouldShowPunchOut(state)) + assertFalse(shouldShowRevoke(state)) + } + + @Test + fun `punched in, not punched out - should show punch out only`() { + val state = PunchUiState(onPunchState = 1, offPunchState = 0) + assertFalse(shouldShowPunchIn(state)) + assertTrue(shouldShowPunchOut(state)) + assertFalse(shouldShowRevoke(state)) + } + + @Test + fun `punched in and out - should show revoke and punch out`() { + val state = PunchUiState(onPunchState = 1, offPunchState = 1) + assertFalse(shouldShowPunchIn(state)) + assertTrue(shouldShowPunchOut(state)) + assertTrue(shouldShowRevoke(state)) + } + + // 按钮显示逻辑(与 PunchPanelView.updateButtons 一致) + private fun shouldShowPunchIn(state: PunchUiState): Boolean = + state.onPunchState == 0 + + private fun shouldShowPunchOut(state: PunchUiState): Boolean = + state.onPunchState == 1 + + private fun shouldShowRevoke(state: PunchUiState): Boolean = + state.onPunchState == 1 && state.offPunchState == 1 +} diff --git a/app/src/test/java/com/xiaoqu/watch/service/manager/NotificationManagerTest.kt b/app/src/test/java/com/xiaoqu/watch/service/manager/NotificationManagerTest.kt new file mode 100644 index 0000000..824bd52 --- /dev/null +++ b/app/src/test/java/com/xiaoqu/watch/service/manager/NotificationManagerTest.kt @@ -0,0 +1,175 @@ +package com.xiaoqu.watch.service.manager + +import com.xiaoqu.watch.data.task.TaskStatistics +import com.xiaoqu.watch.device.screen.ScreenController +import com.xiaoqu.watch.device.sensor.VibrationController +import com.xiaoqu.watch.event.EventBus +import io.mockk.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +/** + * NotificationManager 单元测试 + * 测试:去抖逻辑、任务 ID 解析、消息消费、红点对比 + */ +class NotificationManagerTest { + + private lateinit var manager: NotificationManager + private val vibrationController = mockk(relaxed = true) + private val screenController = mockk(relaxed = true) + private val eventBus = mockk(relaxed = true) + + @Before + fun setup() { + manager = NotificationManager(vibrationController, screenController, eventBus) + } + + // ===== 去抖逻辑 ===== + + @Test + fun `onNewTaskMessage - first message should be handled`() { + val json = """{"id":"100"}""" + val result = manager.onNewTaskMessage(json) + assertTrue(result) + assertEquals(1, manager.pendingCount) + } + + @Test + fun `onNewTaskMessage - duplicate within 1s should be filtered`() { + val json = """{"id":"100"}""" + manager.onNewTaskMessage(json) + // 立即再发一条(间隔 <1s) + val result = manager.onNewTaskMessage(json) + assertFalse(result) + // 只有第一条被处理 + assertEquals(1, manager.pendingCount) + } + + @Test + fun `onNewTaskMessage - should trigger vibration and screen`() { + val json = """{"id":"100"}""" + manager.onNewTaskMessage(json) + verify { vibrationController.executePattern(any()) } + verify { screenController.turnOn() } + } + + // ===== 任务 ID 解析 ===== + + @Test + fun `parseTaskIds - message with direct id field`() { + val json = """{"messageType":1,"id":"200"}""" + manager.onNewTaskMessage(json) + assertEquals(listOf("200"), manager.pendingTaskIds) + } + + @Test + fun `parseTaskIds - message with taskArr array`() { + val json = """{"messageType":1,"taskArr":[{"id":"301"},{"id":"302"}]}""" + manager.onNewTaskMessage(json) + assertEquals(listOf("301", "302"), manager.pendingTaskIds) + } + + @Test + fun `parseTaskIds - message with data array`() { + val json = """{"messageType":1,"data":[{"id":"401"}]}""" + manager.onNewTaskMessage(json) + assertEquals(listOf("401"), manager.pendingTaskIds) + } + + @Test + fun `parseTaskIds - message without id should not be handled`() { + val json = """{"messageType":1,"name":"test"}""" + val result = manager.onNewTaskMessage(json) + assertFalse(result) + assertEquals(0, manager.pendingCount) + } + + @Test + fun `parseTaskIds - malformed json should not crash`() { + val json = "not a json" + val result = manager.onNewTaskMessage(json) + assertFalse(result) + } + + // ===== 去重 ===== + + @Test + fun `onNewTaskMessage - duplicate taskId should not be added twice`() { + // 需要间隔 >1s 绕过去抖,这里直接测内部状态 + val json1 = """{"id":"500"}""" + manager.onNewTaskMessage(json1) + // 模拟第二条消息(通过不同 json 但相同 id,且间隔足够) + // 由于去抖限制,这里只验证单次消息中的去重 + assertEquals(1, manager.pendingCount) + assertTrue(manager.pendingTaskIds.contains("500")) + } + + // ===== 消息消费 ===== + + @Test + fun `consumeAll - should clear all pending tasks`() { + manager.onNewTaskMessage("""{"taskArr":[{"id":"1"},{"id":"2"},{"id":"3"}]}""") + assertEquals(3, manager.pendingCount) + manager.consumeAll() + assertEquals(0, manager.pendingCount) + } + + @Test + fun `consumeByTaskId - should remove specific task`() { + manager.onNewTaskMessage("""{"taskArr":[{"id":"10"},{"id":"20"},{"id":"30"}]}""") + manager.consumeByTaskId("20") + assertEquals(2, manager.pendingCount) + assertFalse(manager.pendingTaskIds.contains("20")) + assertTrue(manager.pendingTaskIds.contains("10")) + assertTrue(manager.pendingTaskIds.contains("30")) + } + + @Test + fun `consumeByTaskId - non-existent id should not crash`() { + manager.onNewTaskMessage("""{"id":"1"}""") + manager.consumeByTaskId("999") + assertEquals(1, manager.pendingCount) + } + + // ===== 红点对比 ===== + + @Test + fun `diffStats - all increased should return all cards`() { + val old = TaskStatistics(waitForTask = 3, treatTask = 2, incompleteTask = 1) + val new = TaskStatistics(waitForTask = 4, treatTask = 3, incompleteTask = 2) + val result = manager.diffStats(old, new) + assertEquals(setOf(2, 3, 4), result) + } + + @Test + fun `diffStats - only waitForTask increased`() { + val old = TaskStatistics(waitForTask = 3, treatTask = 2, incompleteTask = 1) + val new = TaskStatistics(waitForTask = 5, treatTask = 2, incompleteTask = 1) + val result = manager.diffStats(old, new) + assertEquals(setOf(2), result) + } + + @Test + fun `diffStats - no change should return empty`() { + val old = TaskStatistics(waitForTask = 3, treatTask = 2, incompleteTask = 1) + val new = TaskStatistics(waitForTask = 3, treatTask = 2, incompleteTask = 1) + val result = manager.diffStats(old, new) + assertTrue(result.isEmpty()) + } + + @Test + fun `diffStats - decreased should not return card`() { + val old = TaskStatistics(waitForTask = 3, treatTask = 2, incompleteTask = 1) + val new = TaskStatistics(waitForTask = 2, treatTask = 2, incompleteTask = 1) + val result = manager.diffStats(old, new) + assertTrue(result.isEmpty()) + } + + @Test + fun `diffStats - null old stats should return empty`() { + val new = TaskStatistics(waitForTask = 5, treatTask = 3, incompleteTask = 2) + val result = manager.diffStats(null, new) + assertTrue(result.isEmpty()) + } +}