diff --git a/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts b/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts new file mode 100644 index 0000000..3826d52 --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockLocalScheduleReader.ts @@ -0,0 +1,22 @@ +import type { LocalScheduleReader } from '../../application/interfaces'; +import type { LocalReminderSchedule } from '../../domain'; + +import { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules'; + +/** 代替本地数据库端口的固定只读适配器。 */ +export class MockLocalScheduleReader implements LocalScheduleReader { + readonly schedules = MOCK_REMINDER_SCHEDULES; + + async listReminderSchedules(): Promise { + return this.schedules; + } + + async getReminderSchedule(scheduleId: string): Promise { + return this.schedules.find((schedule) => schedule.id === scheduleId) ?? null; + } + + subscribe(listener: (schedules: readonly LocalReminderSchedule[]) => void): () => void { + listener(this.schedules); + return () => undefined; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderApplication.ts b/frontend/src/features/reminder/data/local/MockReminderApplication.ts new file mode 100644 index 0000000..bc717c7 --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderApplication.ts @@ -0,0 +1,105 @@ +import type { + ReminderApplicationDependencies, + ReminderApplicationPort, + ReminderApplicationResult, + ReminderSnoozeRequest, +} from '../../application/interfaces'; +import type { + LocalReminderSchedule, + LocationSample, + ReminderDeliveryReceipt, + ReminderRegistration, + ReminderTrigger, +} from '../../domain'; + +const MOCK_DELIVERY: ReminderDeliveryReceipt = { + delivery_id: 'mock-delivery-001', + schedule_id: 'mock-schedule-time-001', + delivered_at: '2026-08-07T01:00:00.000Z', + channels: ['system_notification', 'popup', 'vibration'], + used_fallback_audio: false, +}; + +function registrationFor(schedule: LocalReminderSchedule): ReminderRegistration { + if (schedule.schedule_type === 'location') { + return { + schedule_id: schedule.id, + time_listener_id: null, + location_listener_id: `mock-location-listener-${schedule.id}`, + alarm_id: null, + }; + } + + return { + schedule_id: schedule.id, + time_listener_id: `mock-time-listener-${schedule.id}`, + location_listener_id: null, + alarm_id: `mock-alarm-${schedule.id}`, + }; +} + +/** 真实协调器完成前供应用外壳使用的应用门面。 */ +export class MockReminderApplication implements ReminderApplicationPort { + constructor(readonly dependencies: ReminderApplicationDependencies) {} + + async start(): Promise { + return Promise.resolve(); + } + + async stop(): Promise { + return Promise.resolve(); + } + + async register(schedule: LocalReminderSchedule): Promise { + return registrationFor(schedule); + } + + async rebuild(): Promise { + const schedules = await this.dependencies.schedules.listReminderSchedules(); + return schedules.map(registrationFor); + } + + async handleTime(_tick: { observed_at: string }): Promise { + return Promise.resolve(); + } + + async handleLocation(_sample: LocationSample): Promise { + return Promise.resolve(); + } + + async deliver(trigger: ReminderTrigger): Promise { + return { ...MOCK_DELIVERY, schedule_id: trigger.schedule_id }; + } + + async confirm(scheduleId: string, confirmedAt: string): Promise { + return { + accepted: true, + schedule_id: scheduleId, + disposition: { + schedule_id: scheduleId, + state: 'confirmed', + updated_at: confirmedAt, + snoozed_until: null, + sync_status: 'pending', + }, + }; + } + + async snooze(request: ReminderSnoozeRequest): Promise { + // 确定性 mock:绝对时间原样回传;相对分钟映射到固定 fixture 时刻。 + const snoozed_until = + 'snooze_minutes' in request ? '2026-08-07T01:10:00.000Z' : request.snooze_until; + + return { + accepted: true, + schedule_id: request.schedule_id, + disposition: { + schedule_id: request.schedule_id, + state: 'snoozed', + updated_at: '2026-08-07T01:00:00.000Z', + snoozed_until, + sync_status: 'pending', + }, + }; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts new file mode 100644 index 0000000..5a7ef1c --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderDispositionSync.ts @@ -0,0 +1,17 @@ +import type { + ReminderConfirmedDisposition, + ReminderDispositionSyncPort, + ReminderDispositionSyncReceipt, +} from '../../application/interfaces'; + +/** 最终确认状态网络同步回调的模拟实现。 */ +export class MockReminderDispositionSync implements ReminderDispositionSyncPort { + async submitConfirmed( + disposition: ReminderConfirmedDisposition, + ): Promise { + return { + schedule_id: disposition.schedule_id, + accepted: true, + }; + } +} diff --git a/frontend/src/features/reminder/data/local/MockReminderStateStore.ts b/frontend/src/features/reminder/data/local/MockReminderStateStore.ts new file mode 100644 index 0000000..187d26c --- /dev/null +++ b/frontend/src/features/reminder/data/local/MockReminderStateStore.ts @@ -0,0 +1,27 @@ +import type { ReminderStateStore } from '../../application/interfaces'; +import type { ReminderDisposition, ReminderRuntimeState } from '../../domain'; + +const MOCK_STATE: ReminderRuntimeState = { + reminder_disposition_state: null, + next_trigger_at: '2026-08-07T01:00:00.000Z', + snoozed_until: null, + geofence_armed: false, + disposition_updated_at: null, + sync_status: 'synced', + recorded_location: null, +}; + +/** 读取结果固定的本地状态空操作适配器,不打开本地数据库。 */ +export class MockReminderStateStore implements ReminderStateStore { + async read(_scheduleId: string): Promise { + return { ...MOCK_STATE }; + } + + async write(_scheduleId: string, _state: ReminderRuntimeState): Promise { + return Promise.resolve(); + } + + async setDisposition(_scheduleId: string, _disposition: ReminderDisposition): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/features/reminder/data/local/index.ts b/frontend/src/features/reminder/data/local/index.ts new file mode 100644 index 0000000..f18ece4 --- /dev/null +++ b/frontend/src/features/reminder/data/local/index.ts @@ -0,0 +1,5 @@ +export { MockLocalScheduleReader } from './MockLocalScheduleReader'; +export { MockReminderApplication } from './MockReminderApplication'; +export { MockReminderDispositionSync } from './MockReminderDispositionSync'; +export { MockReminderStateStore } from './MockReminderStateStore'; +export { MOCK_REMINDER_SCHEDULES } from './mockReminderSchedules'; diff --git a/frontend/src/features/reminder/data/local/mockReminderSchedules.ts b/frontend/src/features/reminder/data/local/mockReminderSchedules.ts new file mode 100644 index 0000000..cc5de05 --- /dev/null +++ b/frontend/src/features/reminder/data/local/mockReminderSchedules.ts @@ -0,0 +1,73 @@ +import type { LocalReminderSchedule } from '../../domain'; + +const MOCK_RUNTIME = { + reminder_disposition_state: null, + next_trigger_at: '2026-08-07T01:00:00.000Z', + snoozed_until: null, + geofence_armed: false, + disposition_updated_at: null, + sync_status: 'synced' as const, + recorded_location: null, +}; + +/** 接入本地数据库读取器前使用的固定快照。 */ +export const MOCK_REMINDER_SCHEDULES: readonly LocalReminderSchedule[] = [ + { + id: 'mock-schedule-time-001', + account_id: 'mock-account-001', + title: '项目例会', + schedule_type: 'time', + schedule_kind: 'once', + is_all_day: false, + start_time: '2026-08-07T01:15:00.000Z', + end_time: null, + timezone: 'Asia/Shanghai', + recurrence_rule: null, + location_name: '203 会议室', + latitude: null, + longitude: null, + geofence_radius_meters: 100, + reminder: { + reminder_type: 'before_start', + reminder_trigger_at: null, + reminder_offset_minutes: 15, + reminder_strength: 'medium', + }, + runtime: { ...MOCK_RUNTIME }, + status: 'active', + revision: 1, + cloud_revision: 1, + updated_at: '2026-08-06T09:00:00.000Z', + }, + { + id: 'mock-schedule-location-001', + account_id: 'mock-account-001', + title: '取车提醒', + schedule_type: 'location', + schedule_kind: 'once', + is_all_day: false, + start_time: null, + end_time: null, + timezone: 'Asia/Shanghai', + recurrence_rule: null, + location_name: '停车场', + latitude: 31.2304, + longitude: 121.4737, + geofence_radius_meters: 100, + reminder: { + reminder_type: 'return_to_recorded_location', + reminder_trigger_at: null, + reminder_offset_minutes: null, + reminder_strength: 'high', + }, + runtime: { + ...MOCK_RUNTIME, + next_trigger_at: null, + recorded_location: { latitude: 31.2304, longitude: 121.4737 }, + }, + status: 'active', + revision: 1, + cloud_revision: 1, + updated_at: '2026-08-06T09:00:00.000Z', + }, +]; diff --git a/frontend/src/features/reminder/index.ts b/frontend/src/features/reminder/index.ts new file mode 100644 index 0000000..60caa6c --- /dev/null +++ b/frontend/src/features/reminder/index.ts @@ -0,0 +1,72 @@ +export type { + DeliveryChannel, + GeoPoint, + LocalReminderSchedule, + LocalScheduleKind, + LocalScheduleStatus, + LocalScheduleType, + LocationSample, + ReminderConfiguration, + ReminderDeliveryReceipt, + ReminderDeliveryRequest, + ReminderDisposition, + ReminderDispositionState, + ReminderRegistration, + ReminderRuntimeState, + ReminderStrength, + ReminderSyncStatus, + ReminderTrigger, + ReminderTriggerReason, + ReminderType, +} from './domain'; +export { DEFAULT_SNOOZE_MINUTES } from './domain'; +export type { + AlarmScheduleReceipt, + AlarmScheduleRequest, + AlarmSchedulerPort, + AudioPlaybackPort, + AudioPlaybackReceipt, + AudioPlaybackRequest, + DeviceCapabilityPort, + DeviceCapabilityStatus, + DevicePermission, + LocalScheduleReader, + LocalTimeTick, + LocationMonitorEvent, + LocationMonitorPort, + LocationWatchHandle, + LocationWatchMode, + LocationWatchRequest, + PopupPort, + PopupReceipt, + PopupRequest, + ReminderApplicationDependencies, + ReminderApplicationPort, + ReminderApplicationResult, + ReminderConfirmedDisposition, + ReminderDeliveryPort, + ReminderDispositionSyncPort, + ReminderDispositionSyncReceipt, + ReminderPresentationAction, + ReminderPresentationReceipt, + ReminderPresenterPort, + ReminderRecoveryPort, + ReminderRecoveryReceipt, + ReminderSnoozeRequest, + ReminderStateStore, + SystemNotificationPort, + SystemNotificationReceipt, + SystemNotificationRequest, + TimeListenerHandle, + TimeListenerOptions, + TimeListenerPort, + VibrationPort, +} from './application'; +export { + MockLocalScheduleReader, + MockReminderApplication, + MockReminderDispositionSync, + MockReminderStateStore, + MOCK_REMINDER_SCHEDULES, +} from './data/local'; +export { MockReminderPresenter } from './presentation'; diff --git a/frontend/src/features/reminder/presentation/MockReminderPresenter.ts b/frontend/src/features/reminder/presentation/MockReminderPresenter.ts new file mode 100644 index 0000000..34bc0fc --- /dev/null +++ b/frontend/src/features/reminder/presentation/MockReminderPresenter.ts @@ -0,0 +1,26 @@ +import type { + ReminderPresenterPort, + ReminderPresentationAction, + ReminderPresentationReceipt, +} from '../application/interfaces'; +import type { ReminderDeliveryRequest } from '../domain'; + +/** 应用级弹窗/悬浮层完成前使用的固定展示器。 */ +export class MockReminderPresenter implements ReminderPresenterPort { + async show(_request: ReminderDeliveryRequest): Promise { + return { + presentation_id: 'mock-presentation-001', + visible: true, + }; + } + + async hide(_scheduleId: string): Promise { + return Promise.resolve(); + } + + onAction( + _listener: (event: { schedule_id: string; action: ReminderPresentationAction }) => void, + ): () => void { + return () => undefined; + } +} diff --git a/frontend/src/features/reminder/presentation/ReminderViewModel.ts b/frontend/src/features/reminder/presentation/ReminderViewModel.ts new file mode 100644 index 0000000..c634d78 --- /dev/null +++ b/frontend/src/features/reminder/presentation/ReminderViewModel.ts @@ -0,0 +1,14 @@ +import type { ReminderPresentationAction } from '../application/interfaces'; +import type { ReminderDeliveryRequest } from '../domain'; + +/** 仅供视图使用的模型;界面悬浮层无需了解端口即可消费。 */ +export type ReminderViewModel = { + schedule_id: string; + title: string; + strength: ReminderDeliveryRequest['strength']; + trigger_reason: ReminderDeliveryRequest['trigger']['reason']; + visible: boolean; + actions: readonly ReminderPresentationAction[]; +}; + +export type ReminderActionHandler = (action: ReminderPresentationAction) => void; diff --git a/frontend/src/features/reminder/presentation/index.ts b/frontend/src/features/reminder/presentation/index.ts new file mode 100644 index 0000000..721b892 --- /dev/null +++ b/frontend/src/features/reminder/presentation/index.ts @@ -0,0 +1,2 @@ +export { MockReminderPresenter } from './MockReminderPresenter'; +export type { ReminderActionHandler, ReminderViewModel } from './ReminderViewModel'; diff --git a/frontend/src/infrastructure/audio/MockAudioPlayback.ts b/frontend/src/infrastructure/audio/MockAudioPlayback.ts new file mode 100644 index 0000000..f59131b --- /dev/null +++ b/frontend/src/infrastructure/audio/MockAudioPlayback.ts @@ -0,0 +1,32 @@ +import type { + AudioPlaybackPort, + AudioPlaybackReceipt, + AudioPlaybackRequest, +} from '../../features/reminder/application/interfaces'; + +/** 固定音频能力:远程语音合成不可用,本地兜底音效可用。 */ +export class MockAudioPlayback implements AudioPlaybackPort { + async isTtsAvailable(): Promise { + return false; + } + + async playTts(_request: AudioPlaybackRequest): Promise { + return { + playback_id: 'mock-tts-playback-001', + played: false, + used_local_fallback: false, + }; + } + + async playLocalFallback(_request: AudioPlaybackRequest): Promise { + return { + playback_id: 'mock-local-sound-001', + played: true, + used_local_fallback: true, + }; + } + + async stop(_scheduleId: string): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/infrastructure/audio/index.ts b/frontend/src/infrastructure/audio/index.ts new file mode 100644 index 0000000..8cc9460 --- /dev/null +++ b/frontend/src/infrastructure/audio/index.ts @@ -0,0 +1 @@ +export { MockAudioPlayback } from './MockAudioPlayback'; diff --git a/frontend/src/infrastructure/location/LocationProvider.ts b/frontend/src/infrastructure/location/LocationProvider.ts new file mode 100644 index 0000000..8493370 --- /dev/null +++ b/frontend/src/infrastructure/location/LocationProvider.ts @@ -0,0 +1,6 @@ +import type { LocationSample } from '../../features/reminder/domain'; + +/** 获取一次当前位置样本的平台适配器。 */ +export interface LocationProvider { + getCurrentSample(): Promise; +} diff --git a/frontend/src/infrastructure/location/MockLocationMonitor.ts b/frontend/src/infrastructure/location/MockLocationMonitor.ts new file mode 100644 index 0000000..b5fb4e6 --- /dev/null +++ b/frontend/src/infrastructure/location/MockLocationMonitor.ts @@ -0,0 +1,57 @@ +import type { + LocationMonitorEvent, + LocationMonitorPort, + LocationWatchHandle, + LocationWatchRequest, +} from '../../features/reminder/application/interfaces'; +import type { LocalReminderSchedule, LocationSample } from '../../features/reminder/domain'; + +import type { LocationProvider } from './LocationProvider'; + +const MOCK_SAMPLE: LocationSample = { + latitude: 31.2304, + longitude: 121.4737, + accuracy_meters: 12, + observed_at: '2026-08-07T01:00:00.000Z', +}; + +function isLocationSchedule(schedule: LocalReminderSchedule): boolean { + return schedule.schedule_type === 'location' && schedule.status === 'active'; +} + +/** 固定定位能力适配器,不访问平台定位接口。 */ +export class MockLocationMonitor implements LocationMonitorPort, LocationProvider { + async watch( + request: LocationWatchRequest, + _listener: (event: LocationMonitorEvent) => void, + ): Promise { + return { + listener_id: `mock-location-listener-${request.schedule_id}`, + schedule_id: request.schedule_id, + }; + } + + async unwatch(_listenerId: string): Promise { + return Promise.resolve(); + } + + async rebuild( + schedules: readonly LocalReminderSchedule[], + _listener: (event: LocationMonitorEvent) => void, + ): Promise { + return schedules.filter(isLocationSchedule).map((schedule) => ({ + listener_id: `mock-location-listener-${schedule.id}`, + schedule_id: schedule.id, + })); + } + + async getLastSample(): Promise { + return { ...MOCK_SAMPLE }; + } + + async getCurrentSample(): Promise { + return { ...MOCK_SAMPLE }; + } +} + +export { MOCK_SAMPLE as MOCK_LOCATION_SAMPLE }; diff --git a/frontend/src/infrastructure/location/index.ts b/frontend/src/infrastructure/location/index.ts new file mode 100644 index 0000000..ef2d2b9 --- /dev/null +++ b/frontend/src/infrastructure/location/index.ts @@ -0,0 +1,2 @@ +export { MockLocationMonitor, MOCK_LOCATION_SAMPLE } from './MockLocationMonitor'; +export type { LocationProvider } from './LocationProvider'; diff --git a/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts b/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts new file mode 100644 index 0000000..847b38e --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockAlarmScheduler.ts @@ -0,0 +1,28 @@ +import type { + AlarmScheduleReceipt, + AlarmScheduleRequest, + AlarmSchedulerPort, +} from '../../features/reminder/application/interfaces'; + +/** 固定闹钟适配器,后续可替换为原生精确闹钟接口。 */ +export class MockAlarmScheduler implements AlarmSchedulerPort { + async schedule(request: AlarmScheduleRequest): Promise { + return { + alarm_id: `mock-alarm-${request.schedule_id}`, + schedule_id: request.schedule_id, + }; + } + + async cancel(_alarmId: string | null): Promise<{ cancelled: boolean }> { + return { cancelled: true }; + } + + async rebuild( + requests: readonly AlarmScheduleRequest[], + ): Promise { + return requests.map((request) => ({ + alarm_id: `mock-alarm-${request.schedule_id}`, + schedule_id: request.schedule_id, + })); + } +} diff --git a/frontend/src/infrastructure/notifications/MockDeviceCapability.ts b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts new file mode 100644 index 0000000..cda5590 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockDeviceCapability.ts @@ -0,0 +1,40 @@ +import type { + DeviceCapabilityPort, + DeviceCapabilityStatus, + DevicePermission, +} from '../../features/reminder/application/interfaces'; + +const MOCK_PERMISSIONS: Readonly> = { + notifications: true, + exact_alarm: true, + overlay: false, + full_screen: true, + battery_optimization: false, + location_foreground: true, + location_background: false, +}; + +const MOCK_STATUS: DeviceCapabilityStatus = { + platform: 'android', + supported: true, + permissions: MOCK_PERMISSIONS, + background_execution: false, +}; + +/** 原生模块接入前使用的固定设备能力状态。 */ +export class MockDeviceCapability implements DeviceCapabilityPort { + async getStatus(): Promise { + return { ...MOCK_STATUS, permissions: { ...MOCK_STATUS.permissions } }; + } + + async requestPermission(permission: DevicePermission): Promise { + // 与 getStatus() 保持一致:返回该权限的固定值,不假装 grant 成功。 + return MOCK_PERMISSIONS[permission]; + } + + async openSettings(_permission: DevicePermission): Promise { + return true; + } +} + +export { MOCK_STATUS as MOCK_DEVICE_CAPABILITY_STATUS }; diff --git a/frontend/src/infrastructure/notifications/MockNotificationChannels.ts b/frontend/src/infrastructure/notifications/MockNotificationChannels.ts new file mode 100644 index 0000000..d42c7a9 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockNotificationChannels.ts @@ -0,0 +1,39 @@ +import type { + PopupPort, + PopupReceipt, + PopupRequest, + SystemNotificationPort, + SystemNotificationReceipt, + SystemNotificationRequest, + VibrationPort, +} from '../../features/reminder/application/interfaces'; + +export class MockSystemNotification implements SystemNotificationPort { + async show(request: SystemNotificationRequest): Promise { + return { notification_id: request.notification_id, shown: true }; + } + + async cancel(_notificationId: string): Promise { + return Promise.resolve(); + } +} + +export class MockPopup implements PopupPort { + async show(request: PopupRequest): Promise { + return { popup_id: request.popup_id, visible: true }; + } + + async dismiss(_popupId: string): Promise { + return Promise.resolve(); + } +} + +export class MockVibration implements VibrationPort { + async vibrate(): Promise { + return Promise.resolve(); + } + + async stop(): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/infrastructure/notifications/MockReminderDelivery.ts b/frontend/src/infrastructure/notifications/MockReminderDelivery.ts new file mode 100644 index 0000000..f811570 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockReminderDelivery.ts @@ -0,0 +1,26 @@ +import type { ReminderDeliveryPort } from '../../features/reminder/application/interfaces'; +import type { + ReminderDeliveryReceipt, + ReminderDeliveryRequest, +} from '../../features/reminder/domain'; + +const MOCK_RECEIPT: ReminderDeliveryReceipt = { + delivery_id: 'mock-delivery-001', + schedule_id: 'mock-schedule-time-001', + delivered_at: '2026-08-07T01:00:00.000Z', + channels: ['system_notification'], + used_fallback_audio: false, +}; + +/** 系统通知送达使用的固定通知边界。 */ +export class MockReminderDelivery implements ReminderDeliveryPort { + async deliver(request: ReminderDeliveryRequest): Promise { + return { ...MOCK_RECEIPT, schedule_id: request.schedule_id }; + } + + async dismiss(_scheduleId: string): Promise { + return Promise.resolve(); + } +} + +export { MOCK_RECEIPT as MOCK_REMINDER_DELIVERY_RECEIPT }; diff --git a/frontend/src/infrastructure/notifications/MockReminderRecovery.ts b/frontend/src/infrastructure/notifications/MockReminderRecovery.ts new file mode 100644 index 0000000..34be1b0 --- /dev/null +++ b/frontend/src/infrastructure/notifications/MockReminderRecovery.ts @@ -0,0 +1,15 @@ +import type { + ReminderRecoveryPort, + ReminderRecoveryReceipt, +} from '../../features/reminder/application/interfaces'; + +/** 固定重启恢复适配器,后续可替换为安卓启动接收器。 */ +export class MockReminderRecovery implements ReminderRecoveryPort { + async registerForRestart(): Promise { + return { registered: true, recovery_id: 'mock-recovery-001' }; + } + + async restoreAfterRestart(): Promise { + return { registered: true, recovery_id: 'mock-recovery-001' }; + } +} diff --git a/frontend/src/infrastructure/notifications/index.ts b/frontend/src/infrastructure/notifications/index.ts new file mode 100644 index 0000000..8eaf098 --- /dev/null +++ b/frontend/src/infrastructure/notifications/index.ts @@ -0,0 +1,5 @@ +export { MockAlarmScheduler } from './MockAlarmScheduler'; +export { MockPopup, MockSystemNotification, MockVibration } from './MockNotificationChannels'; +export { MockReminderRecovery } from './MockReminderRecovery'; +export { MockReminderDelivery, MOCK_REMINDER_DELIVERY_RECEIPT } from './MockReminderDelivery'; +export { MockDeviceCapability, MOCK_DEVICE_CAPABILITY_STATUS } from './MockDeviceCapability'; diff --git a/frontend/src/shared/time/MockClock.ts b/frontend/src/shared/time/MockClock.ts new file mode 100644 index 0000000..edc7ff1 --- /dev/null +++ b/frontend/src/shared/time/MockClock.ts @@ -0,0 +1,12 @@ +/** 用于确定性测试和模拟组合的最小时钟端口。 */ +export interface Clock { + nowIso(): string; +} + +export class MockClock implements Clock { + constructor(private readonly value = '2026-08-07T01:00:00.000Z') {} + + nowIso(): string { + return this.value; + } +} diff --git a/frontend/src/shared/time/MockTimeListener.ts b/frontend/src/shared/time/MockTimeListener.ts new file mode 100644 index 0000000..e77e36e --- /dev/null +++ b/frontend/src/shared/time/MockTimeListener.ts @@ -0,0 +1,20 @@ +import type { + LocalTimeTick, + TimeListenerHandle, + TimeListenerOptions, + TimeListenerPort, +} from '../../features/reminder/application/interfaces'; + +/** 固定时间监听边界,替换前不会发出平台事件。 */ +export class MockTimeListener implements TimeListenerPort { + async start( + _listener: (tick: LocalTimeTick) => void, + _options?: TimeListenerOptions, + ): Promise { + return { listener_id: 'mock-time-listener-001' }; + } + + async stop(_listenerId: string): Promise { + return Promise.resolve(); + } +} diff --git a/frontend/src/shared/time/index.ts b/frontend/src/shared/time/index.ts new file mode 100644 index 0000000..1924ce7 --- /dev/null +++ b/frontend/src/shared/time/index.ts @@ -0,0 +1,3 @@ +export { MockClock } from './MockClock'; +export type { Clock } from './MockClock'; +export { MockTimeListener } from './MockTimeListener';