feat(fasting): 重构断食通知系统并增强可靠性
- 新增 useFastingNotifications hook 统一管理通知状态和同步逻辑 - 实现四阶段通知提醒:开始前30分钟、开始时、结束前30分钟、结束时 - 添加通知验证机制,确保通知正确设置和避免重复 - 新增 NotificationErrorAlert 组件显示通知错误并提供重试选项 - 实现断食计划持久化存储,应用重启后自动恢复 - 添加开发者测试面板用于验证通知系统可靠性 - 优化通知同步策略,支持选择性更新减少不必要的操作 - 修复个人页面编辑按钮样式问题 - 更新应用版本号至 1.0.18
This commit is contained in:
341
utils/fastingNotificationTest.ts
Normal file
341
utils/fastingNotificationTest.ts
Normal file
@@ -0,0 +1,341 @@
|
||||
import { FastingPlan } from '@/constants/Fasting';
|
||||
import {
|
||||
ensureFastingNotificationsReady,
|
||||
resyncFastingNotifications,
|
||||
verifyFastingNotifications
|
||||
} from '@/services/fastingNotifications';
|
||||
import { notificationService } from '@/services/notifications';
|
||||
import { FastingSchedule } from '@/store/fastingSlice';
|
||||
import { FastingNotificationIds } from '@/utils/fasting';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* 断食通知系统测试工具
|
||||
* 用于验证通知系统在各种场景下的可靠性
|
||||
*/
|
||||
export class FastingNotificationTester {
|
||||
private testResults: Array<{
|
||||
testName: string;
|
||||
passed: boolean;
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
}> = [];
|
||||
|
||||
private logResult(testName: string, passed: boolean, message: string) {
|
||||
const result = {
|
||||
testName,
|
||||
passed,
|
||||
message,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
this.testResults.push(result);
|
||||
|
||||
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${testName}: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试结果
|
||||
*/
|
||||
getTestResults() {
|
||||
return this.testResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除测试结果
|
||||
*/
|
||||
clearResults() {
|
||||
this.testResults = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知权限检查
|
||||
*/
|
||||
async testNotificationPermissions() {
|
||||
try {
|
||||
const ready = await ensureFastingNotificationsReady();
|
||||
this.logResult(
|
||||
'通知权限检查',
|
||||
ready,
|
||||
ready ? '通知权限已授予' : '通知权限未授予'
|
||||
);
|
||||
return ready;
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'通知权限检查',
|
||||
false,
|
||||
`权限检查失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知安排
|
||||
*/
|
||||
async testNotificationScheduling(plan: FastingPlan) {
|
||||
const now = dayjs();
|
||||
const start = now.add(1, 'hour');
|
||||
const end = start.add(plan.fastingHours, 'hour');
|
||||
|
||||
const schedule: FastingSchedule = {
|
||||
planId: plan.id,
|
||||
startISO: start.toISOString(),
|
||||
endISO: end.toISOString(),
|
||||
createdAtISO: now.toISOString(),
|
||||
updatedAtISO: now.toISOString(),
|
||||
origin: 'manual',
|
||||
};
|
||||
|
||||
try {
|
||||
const notificationIds = await resyncFastingNotifications({
|
||||
schedule,
|
||||
plan,
|
||||
previousIds: {},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const hasAllIds = !!(notificationIds.preStartId && notificationIds.startId &&
|
||||
notificationIds.preEndId && notificationIds.endId);
|
||||
|
||||
this.logResult(
|
||||
'通知安排测试',
|
||||
hasAllIds,
|
||||
hasAllIds
|
||||
? '成功安排所有四个通知点'
|
||||
: `缺少通知ID: ${JSON.stringify(notificationIds)}`
|
||||
);
|
||||
|
||||
return { success: hasAllIds, notificationIds };
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'通知安排测试',
|
||||
false,
|
||||
`通知安排失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
return { success: false, notificationIds: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知验证
|
||||
*/
|
||||
async testNotificationVerification(
|
||||
schedule: FastingSchedule,
|
||||
plan: FastingPlan,
|
||||
notificationIds: FastingNotificationIds
|
||||
) {
|
||||
try {
|
||||
const { isValid, updatedIds } = await verifyFastingNotifications({
|
||||
schedule,
|
||||
plan,
|
||||
storedIds: notificationIds,
|
||||
});
|
||||
|
||||
this.logResult(
|
||||
'通知验证测试',
|
||||
isValid,
|
||||
isValid ? '通知验证通过' : '通知验证失败,已重新同步'
|
||||
);
|
||||
|
||||
return { isValid, updatedIds };
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'通知验证测试',
|
||||
false,
|
||||
`通知验证失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
return { isValid: false, updatedIds: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知取消
|
||||
*/
|
||||
async testNotificationCancellation(notificationIds: FastingNotificationIds) {
|
||||
try {
|
||||
// 取消所有通知
|
||||
const cancelPromises = [];
|
||||
if (notificationIds.preStartId) {
|
||||
cancelPromises.push(notificationService.cancelNotification(notificationIds.preStartId));
|
||||
}
|
||||
if (notificationIds.startId) {
|
||||
cancelPromises.push(notificationService.cancelNotification(notificationIds.startId));
|
||||
}
|
||||
if (notificationIds.preEndId) {
|
||||
cancelPromises.push(notificationService.cancelNotification(notificationIds.preEndId));
|
||||
}
|
||||
if (notificationIds.endId) {
|
||||
cancelPromises.push(notificationService.cancelNotification(notificationIds.endId));
|
||||
}
|
||||
|
||||
await Promise.all(cancelPromises);
|
||||
|
||||
// 验证通知是否已取消
|
||||
const scheduledNotifications = await notificationService.getAllScheduledNotifications();
|
||||
const remainingIds = scheduledNotifications.map(n => n.identifier);
|
||||
const cancelledIds = Object.values(notificationIds).filter(id =>
|
||||
id && !remainingIds.includes(id)
|
||||
);
|
||||
|
||||
const allCancelled = cancelledIds.length === Object.values(notificationIds).filter(id => id).length;
|
||||
|
||||
this.logResult(
|
||||
'通知取消测试',
|
||||
allCancelled,
|
||||
allCancelled
|
||||
? '所有通知已成功取消'
|
||||
: `部分通知未取消: ${JSON.stringify(cancelledIds)}`
|
||||
);
|
||||
|
||||
return allCancelled;
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'通知取消测试',
|
||||
false,
|
||||
`通知取消失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试边界情况
|
||||
*/
|
||||
async testEdgeCases(plan: FastingPlan) {
|
||||
const now = dayjs();
|
||||
|
||||
// 测试1: 已经过期的断食期
|
||||
const pastStart = now.subtract(2, 'hour');
|
||||
const pastEnd = pastStart.add(plan.fastingHours, 'hour');
|
||||
|
||||
const pastSchedule: FastingSchedule = {
|
||||
planId: plan.id,
|
||||
startISO: pastStart.toISOString(),
|
||||
endISO: pastEnd.toISOString(),
|
||||
createdAtISO: now.toISOString(),
|
||||
updatedAtISO: now.toISOString(),
|
||||
origin: 'manual',
|
||||
};
|
||||
|
||||
try {
|
||||
const pastNotificationIds = await resyncFastingNotifications({
|
||||
schedule: pastSchedule,
|
||||
plan,
|
||||
previousIds: {},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const hasNoIds = Object.values(pastNotificationIds).every(id => !id);
|
||||
|
||||
this.logResult(
|
||||
'过期断食期测试',
|
||||
hasNoIds,
|
||||
hasNoIds ? '正确处理过期断食期,未安排通知' : '错误地为过期断食期安排了通知'
|
||||
);
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'过期断食期测试',
|
||||
false,
|
||||
`过期断食期测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
}
|
||||
|
||||
// 测试2: 即将开始的断食期(少于30分钟)
|
||||
const imminentStart = now.add(15, 'minute');
|
||||
const imminentEnd = imminentStart.add(plan.fastingHours, 'hour');
|
||||
|
||||
const imminentSchedule: FastingSchedule = {
|
||||
planId: plan.id,
|
||||
startISO: imminentStart.toISOString(),
|
||||
endISO: imminentEnd.toISOString(),
|
||||
createdAtISO: now.toISOString(),
|
||||
updatedAtISO: now.toISOString(),
|
||||
origin: 'manual',
|
||||
};
|
||||
|
||||
try {
|
||||
const imminentNotificationIds = await resyncFastingNotifications({
|
||||
schedule: imminentSchedule,
|
||||
plan,
|
||||
previousIds: {},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// 应该只有开始时、结束前30分钟和结束时的通知
|
||||
const hasCorrectIds = !imminentNotificationIds.preStartId &&
|
||||
!!imminentNotificationIds.startId &&
|
||||
!!imminentNotificationIds.preEndId &&
|
||||
!!imminentNotificationIds.endId;
|
||||
|
||||
this.logResult(
|
||||
'即将开始断食期测试',
|
||||
hasCorrectIds,
|
||||
hasCorrectIds
|
||||
? '正确处理即将开始的断食期,只安排了必要的通知'
|
||||
: '通知安排不正确'
|
||||
);
|
||||
} catch (error) {
|
||||
this.logResult(
|
||||
'即将开始断食期测试',
|
||||
false,
|
||||
`即将开始断食期测试失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行完整的测试套件
|
||||
*/
|
||||
async runFullTestSuite(plan: FastingPlan) {
|
||||
console.log('开始运行断食通知系统测试套件...');
|
||||
this.clearResults();
|
||||
|
||||
// 1. 测试通知权限
|
||||
const hasPermission = await this.testNotificationPermissions();
|
||||
if (!hasPermission) {
|
||||
console.log('通知权限未授予,跳过其他测试');
|
||||
return this.getTestResults();
|
||||
}
|
||||
|
||||
// 2. 测试通知安排
|
||||
const { success: schedulingSuccess, notificationIds } = await this.testNotificationScheduling(plan);
|
||||
if (!schedulingSuccess) {
|
||||
console.log('通知安排失败,跳过后续测试');
|
||||
return this.getTestResults();
|
||||
}
|
||||
|
||||
// 3. 创建测试用的断食计划
|
||||
const now = dayjs();
|
||||
const start = now.add(1, 'hour');
|
||||
const end = start.add(plan.fastingHours, 'hour');
|
||||
|
||||
const schedule: FastingSchedule = {
|
||||
planId: plan.id,
|
||||
startISO: start.toISOString(),
|
||||
endISO: end.toISOString(),
|
||||
createdAtISO: now.toISOString(),
|
||||
updatedAtISO: now.toISOString(),
|
||||
origin: 'manual',
|
||||
};
|
||||
|
||||
// 4. 测试通知验证
|
||||
await this.testNotificationVerification(schedule, plan, notificationIds);
|
||||
|
||||
// 5. 测试边界情况
|
||||
await this.testEdgeCases(plan);
|
||||
|
||||
// 6. 测试通知取消
|
||||
await this.testNotificationCancellation(notificationIds);
|
||||
|
||||
const results = this.getTestResults();
|
||||
const passedCount = results.filter(r => r.passed).length;
|
||||
const totalCount = results.length;
|
||||
|
||||
console.log(`测试完成: ${passedCount}/${totalCount} 通过`);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出测试实例
|
||||
export const fastingNotificationTester = new FastingNotificationTester();
|
||||
Reference in New Issue
Block a user