'Europe/London', 'app.platform_timezone' => 'Asia/Shanghai', ]); } #[DataProvider('waitingDurationProvider')] public function test_waiting_duration_uses_actual_full_hours( string $startedAt, string $expected, ): void { $now = CarbonImmutable::parse('2026-10-30 12:00:00', 'Asia/Shanghai'); $this->assertSame($expected, OaProcessTime::formatWaiting($startedAt, $now)); } public static function waitingDurationProvider(): array { return [ 'less than one hour' => ['2026-10-30 11:01:00', '不足1小时'], 'one hour' => ['2026-10-30 11:00:00', '1小时'], 'eleven hours' => ['2026-10-30 00:01:00', '11小时'], 'twelve hours' => ['2026-10-30 00:00:00', '12小时'], 'one day' => ['2026-10-29 12:00:00', '1天'], 'one day twelve hours' => ['2026-10-29 00:00:00', '1天12小时'], 'two days' => ['2026-10-28 12:00:00', '2天'], 'three days' => ['2026-10-27 12:00:00', '3天'], ]; } public function test_task_timestamp_is_converted_from_utc_to_platform_timezone(): void { $this->assertSame( '2026-10-30 02:50', OaProcessTime::formatDateTime('2026-10-29T18:50:00Z'), ); $this->assertSame( '12小时', OaProcessTime::formatWaiting( '2026-10-29T18:50:00Z', CarbonImmutable::parse('2026-10-30 14:50:00', 'Asia/Shanghai'), ), ); } public function test_dingtalk_task_suffix_is_treated_as_local_wall_time(): void { $this->assertSame( '10:56', OaProcessTime::parseDingTalkTask('2026-10-30T10:56Z')?->format('H:i'), ); } public function test_future_timestamp_is_not_shown_as_less_than_one_minute(): void { $now = CarbonImmutable::parse('2026-10-30 12:00:00', 'Asia/Shanghai'); $this->assertSame( '-', OaProcessTime::formatWaiting('2026-10-30 12:01:00', $now), ); } }