@ray-js/lamp-schedule-core 1.0.1-beta-10 → 1.0.2-beta-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/conflict/ConflictResolver.js +48 -21
  2. package/lib/conflict/__test__/ConflictResolver.test.js +49 -0
  3. package/lib/conflict/__test__/scheduleDataManager.test.js +66 -0
  4. package/lib/conflict/__test__/transform.test.js +5 -1
  5. package/lib/conflict/index.js +27 -7
  6. package/lib/conflict/scheduleDataManager.js +28 -33
  7. package/lib/context/rhythms/actions.d.ts +13 -17
  8. package/lib/context/rhythms/reducer.d.ts +2 -23
  9. package/lib/context/rhythms/reducer.js +7 -8
  10. package/lib/context/schedule/reducer.d.ts +5 -2
  11. package/lib/dpParser/__test__/autoDispatch.test.d.ts +1 -0
  12. package/lib/dpParser/__test__/autoDispatch.test.js +45 -0
  13. package/lib/dpParser/__test__/rhythms.test.js +90 -14
  14. package/lib/dpParser/__test__/rtcTimer.test.js +35 -0
  15. package/lib/dpParser/__test__/sleep.test.js +17 -0
  16. package/lib/dpParser/__test__/wakeup.test.js +17 -4
  17. package/lib/dpParser/__test__/wakeupSigmesh.test.d.ts +1 -0
  18. package/lib/dpParser/__test__/wakeupSigmesh.test.js +126 -0
  19. package/lib/hooks/__test__/useBaseLightDp.test.d.ts +1 -0
  20. package/lib/hooks/__test__/useBaseLightDp.test.js +251 -0
  21. package/lib/hooks/__test__/useCommonSupport.test.d.ts +1 -0
  22. package/lib/hooks/__test__/useCommonSupport.test.js +204 -0
  23. package/lib/hooks/__test__/useTimerFlushList.test.d.ts +1 -0
  24. package/lib/hooks/__test__/useTimerFlushList.test.js +117 -0
  25. package/lib/hooks/__test__/useTimerOperate.test.js +239 -5
  26. package/lib/hooks/__test__/useTimerOperateLocal.test.js +161 -53
  27. package/lib/types/rhythms.d.ts +25 -13
  28. package/lib/types/ty.d.ts +3 -0
  29. package/lib/utils/ScheduleDataSync.js +4 -1
  30. package/lib/utils/ScheduleLogger.js +1 -1
  31. package/lib/utils/__test__/ScheduleDataSync.test.d.ts +11 -0
  32. package/lib/utils/__test__/ScheduleDataSync.test.js +137 -0
  33. package/lib/utils/__test__/ScheduleEmit.test.d.ts +1 -0
  34. package/lib/utils/__test__/ScheduleEmit.test.js +35 -0
  35. package/lib/utils/__test__/ScheduleSupport.test.js +288 -154
  36. package/package.json +1 -1
@@ -62,18 +62,28 @@ function doRangesOverlap(aStart, aEnd, bStart, bEnd) {
62
62
  return true;
63
63
  }
64
64
 
65
- // 情况2: 两个范围都跨天
66
- // 当两个范围都跨天时,它们一定会重叠,因为都覆盖了午夜时间点
67
- if (aIsCrossDay && bIsCrossDay) {
65
+ // 情况2: 单时间点的特殊处理
66
+ // 如果两个时间范围都是单点(开始等于结束),且时间点相同,则认为冲突
67
+ if (aStartMin === aEndMin && bStartMin === bEndMin && aStartMin === bStartMin) {
68
68
  return true;
69
69
  }
70
70
 
71
- // 情况3: 一个范围跨天,一个不跨天
72
- // 这种情况下,我们已经将跨天范围标准化为连续分钟数
73
- // 所以可以直接使用标准区间重叠判断
71
+ // 情况3: 单点与时间范围的重叠判断
72
+ // 如果一个是单点,一个是范围,单点在范围内(包含边界)则冲突
73
+ if (aStartMin === aEndMin) {
74
+ // a是单点,判断是否在b范围内(考虑跨天场景)
75
+ const normalizedPoint = bIsCrossDay && aStartMin < bStartMin ? aStartMin + 1440 : aStartMin;
76
+ return normalizedPoint >= bStartMin && normalizedPoint <= bEndMin;
77
+ }
78
+ if (bStartMin === bEndMin) {
79
+ // b是单点,判断是否在a范围内(考虑跨天场景)
80
+ const normalizedPoint = aIsCrossDay && bStartMin < aStartMin ? bStartMin + 1440 : bStartMin;
81
+ return normalizedPoint >= aStartMin && normalizedPoint <= aEndMin;
82
+ }
74
83
 
75
- // 情况4: 两个范围都不跨天
84
+ // 情况4: 标准区间重叠判断
76
85
  // 使用标准区间重叠判断: [a1,a2] 和 [b1,b2] 重叠当且仅当 a2 > b1 && b2 > a1
86
+ // 注意:这里已经处理了跨天情况(通过标准化为连续分钟数)
77
87
  return aEndMin > bStartMin && bEndMin > aStartMin;
78
88
  }
79
89
 
@@ -127,18 +137,39 @@ function convertToTimeRanges(schedules) {
127
137
  const endMinutes = parseTimeToMinutes(endTime);
128
138
  const isCrossDay = endMinutes < startMinutes;
129
139
 
130
- // 如果定时状态为关闭,则设置星期,忽略冲突判断
131
- const dayOfWeek = daysOfWeek[isCrossDay ? (week + 1) % 7 : week];
132
- return [{
140
+ // 单次任务使用当前星期
141
+ const currentDayOfWeek = daysOfWeek[week];
142
+ const result = [];
143
+
144
+ // 添加当天的时间范围
145
+ result.push({
133
146
  id: schedule.id,
134
147
  type: schedule.type,
135
- dayOfWeek: dayOfWeek,
148
+ dayOfWeek: currentDayOfWeek,
136
149
  status: schedule.data.status,
137
150
  startMinutes,
138
151
  endMinutes,
139
152
  originalSchedule: schedule,
140
153
  isCrossDay
141
- }];
154
+ });
155
+
156
+ // 如果是跨天任务,还需要添加下一天的时间范围(从午夜到结束时间)
157
+ if (isCrossDay) {
158
+ const nextDayOfWeek = daysOfWeek[(week + 1) % 7];
159
+ result.push({
160
+ id: schedule.id,
161
+ type: schedule.type,
162
+ dayOfWeek: nextDayOfWeek,
163
+ status: schedule.data.status,
164
+ startMinutes: 0,
165
+ // 从午夜开始
166
+ endMinutes: endMinutes,
167
+ // 到结束时间
168
+ originalSchedule: schedule,
169
+ isCrossDay: false // 这部分不再是跨天
170
+ });
171
+ }
172
+ return result;
142
173
  } catch (error) {
143
174
  scheduleLogger.error('Error processing single task:', error, schedule);
144
175
  return [];
@@ -252,21 +283,17 @@ export function checkConflicts(scheduleList, currentSchedule) {
252
283
  scheduleLogger.debug('checkConflicts otherRanges:', otherRanges);
253
284
  currentRanges.forEach(current => {
254
285
  otherRanges.forEach(other => {
255
- var _rule$shouldIgnore, _current$originalSche, _current$originalSche2;
286
+ var _rule$shouldIgnore, _current$originalSche;
256
287
  // 应用规则中的忽略条件
257
288
  if ((_rule$shouldIgnore = rule.shouldIgnore) !== null && _rule$shouldIgnore !== void 0 && _rule$shouldIgnore.call(rule, current, other)) {
258
289
  return;
259
290
  }
260
291
  const currentPreId = current === null || current === void 0 || (_current$originalSche = current.originalSchedule) === null || _current$originalSche === void 0 ? void 0 : _current$originalSche.prevId;
261
- const currentId = current === null || current === void 0 || (_current$originalSche2 = current.originalSchedule) === null || _current$originalSche2 === void 0 ? void 0 : _current$originalSche2.id;
262
292
  const otherId = other.id;
263
- // 针对定时,如果当前定时自身修改 且 与自身冲突 则忽略
264
- if (currentPreId && currentId === currentPreId && current.type === 'timer') {
265
- return;
266
- }
267
- // FIXME:还是存在bug
268
- // 如果当前定时自身修改 且 与自身冲突 并且其他定时不等于当前定时id 则忽略
269
- if (currentPreId && currentId === currentPreId && otherId !== currentId) {
293
+
294
+ // 如果是更新操作,忽略与旧版本自身的冲突判断
295
+ // 这里通过 prevId 来标识这是一个更新操作,并且要忽略与旧版本的冲突
296
+ if (currentPreId && otherId === currentPreId) {
270
297
  return;
271
298
  }
272
299
  if (current.dayOfWeek && current.status && other.status && current.dayOfWeek === other.dayOfWeek && rule.isConflict(current, other)) {
@@ -88,6 +88,55 @@ describe('ConflictResolver', () => {
88
88
  const conflicts = checkConflicts([schedule1], schedule2);
89
89
  expect(conflicts.length).toBe(1);
90
90
  });
91
+ it('should detect conflicts when cross-day range overlaps single point schedule', () => {
92
+ const crossDaySchedule = {
93
+ id: 'timer_1',
94
+ type: EScheduleFunctionType.TIMER,
95
+ data: {
96
+ status: true,
97
+ weeks: [1, 0, 0, 0, 0, 0, 0],
98
+ startTime: '23:00',
99
+ endTime: '01:00'
100
+ }
101
+ };
102
+ const singlePointSchedule = {
103
+ id: 'timer_2',
104
+ type: EScheduleFunctionType.TIMER,
105
+ data: {
106
+ status: true,
107
+ weeks: [0, 1, 0, 0, 0, 0, 0],
108
+ startTime: '00:30',
109
+ endTime: '00:30'
110
+ }
111
+ };
112
+ const conflicts = checkConflicts([crossDaySchedule], singlePointSchedule);
113
+ expect(conflicts.length).toBe(1);
114
+ expect(conflicts[0].other).toBe(crossDaySchedule);
115
+ });
116
+ it('should not detect conflicts when single point lies outside cross-day range', () => {
117
+ const crossDaySchedule = {
118
+ id: 'timer_1',
119
+ type: EScheduleFunctionType.TIMER,
120
+ data: {
121
+ status: true,
122
+ weeks: [1, 0, 0, 0, 0, 0, 0],
123
+ startTime: '23:00',
124
+ endTime: '01:00'
125
+ }
126
+ };
127
+ const singlePointSchedule = {
128
+ id: 'timer_2',
129
+ type: EScheduleFunctionType.TIMER,
130
+ data: {
131
+ status: true,
132
+ weeks: [0, 1, 0, 0, 0, 0, 0],
133
+ startTime: '01:30',
134
+ endTime: '01:30'
135
+ }
136
+ };
137
+ const conflicts = checkConflicts([crossDaySchedule], singlePointSchedule);
138
+ expect(conflicts.length).toBe(0);
139
+ });
91
140
 
92
141
  // 测试关闭状态的计划不会产生冲突
93
142
  it('should not detect conflicts with schedules that are turned off', () => {
@@ -100,6 +100,72 @@ describe('ScheduleDataManager', () => {
100
100
  expect(data.length).toBe(1);
101
101
  expect(data[0]).toEqual(schedule2); // 应该是更新后的数据
102
102
  });
103
+ it('should keep only the latest item when duplicate ids are provided in one call', () => {
104
+ const instance = ScheduleDataManager.getInstance();
105
+ const first = {
106
+ id: 'dup',
107
+ type: EScheduleFunctionType.TIMER,
108
+ data: {
109
+ status: true,
110
+ weeks: [1, 0, 0, 0, 0, 0, 0],
111
+ startTime: '08:00',
112
+ endTime: '08:30'
113
+ }
114
+ };
115
+ const second = {
116
+ id: 'dup',
117
+ type: EScheduleFunctionType.TIMER,
118
+ data: {
119
+ status: true,
120
+ weeks: [1, 0, 0, 0, 0, 0, 0],
121
+ startTime: '09:00',
122
+ endTime: '09:30'
123
+ }
124
+ };
125
+ instance.setData([first, second]);
126
+ const data = instance.getData();
127
+ expect(data.length).toBe(1);
128
+ expect(data[0]).toEqual(second);
129
+ });
130
+ it('should flatten nested arrays and deduplicate by id with last occurrence winning', () => {
131
+ const instance = ScheduleDataManager.getInstance();
132
+ const version1 = {
133
+ id: 'nested',
134
+ type: EScheduleFunctionType.TIMER,
135
+ data: {
136
+ status: true,
137
+ weeks: [1, 0, 0, 0, 0, 0, 0],
138
+ startTime: '07:00',
139
+ endTime: '07:30'
140
+ }
141
+ };
142
+ const version2 = {
143
+ id: 'nested',
144
+ type: EScheduleFunctionType.TIMER,
145
+ data: {
146
+ status: true,
147
+ weeks: [1, 0, 0, 0, 0, 0, 0],
148
+ startTime: '10:00',
149
+ endTime: '10:30'
150
+ }
151
+ };
152
+ const another = {
153
+ id: 'another',
154
+ type: EScheduleFunctionType.TIMER,
155
+ data: {
156
+ status: true,
157
+ weeks: [0, 1, 0, 0, 0, 0, 0],
158
+ startTime: '12:00',
159
+ endTime: '12:30'
160
+ }
161
+ };
162
+ instance.setData([[version1], [version2, another]]);
163
+ const data = instance.getData();
164
+ expect(data.length).toBe(2);
165
+ expect(data).toContainEqual(version2);
166
+ expect(data).toContainEqual(another);
167
+ expect(data).not.toContainEqual(version1);
168
+ });
103
169
 
104
170
  // 测试清空数据
105
171
  it('should clear all data', () => {
@@ -48,7 +48,11 @@ describe('transform', () => {
48
48
  status: true,
49
49
  weeks: [1, 0, 0, 0, 0, 0, 0],
50
50
  startTime: '10:00',
51
- endTime: '10:00'
51
+ endTime: '10:00',
52
+ dps: {},
53
+ aliasName: 'aliasName',
54
+ isAppPush: false,
55
+ id: '1'
52
56
  });
53
57
  expect(result.id).toMatch(/^timer_/);
54
58
  });
@@ -30,6 +30,13 @@ export class Conflict {
30
30
  const instance = ScheduleDataManager.getInstance();
31
31
  const preList = instance.getData() || [];
32
32
  const [cur] = transScheduleListToConflictList([current]);
33
+ if (!cur) {
34
+ scheduleLogger.error('Conflict.add: 无法转换当前日程', current);
35
+ return {
36
+ isConflict: false,
37
+ conflictList: []
38
+ };
39
+ }
33
40
  scheduleLogger.debug('Conflict.add cur, current:', cur, current);
34
41
  const conflictList = checkConflicts(preList, cur, Conflict.rule);
35
42
  const isConflict = conflictList.length > 0;
@@ -69,17 +76,30 @@ export class Conflict {
69
76
  const instance = ScheduleDataManager.getInstance();
70
77
  const [cur, prev] = transScheduleListToConflictList([current, prevSchedule]);
71
78
  scheduleLogger.debug('Conflict.update1:', cur, prev);
79
+
80
+ // 先删除旧数据,避免在冲突检测时干扰
72
81
  const preList = instance.getData() || [];
73
- const _current = _objectSpread(_objectSpread({}, cur), {}, {
82
+ let preItem;
83
+ if (prev && prev.id) {
84
+ preItem = preList.find(i => i.id === prev.id);
85
+ if (preItem) {
86
+ instance.deleteData(preItem);
87
+ }
88
+ }
89
+
90
+ // 使用清理后的列表进行冲突检测
91
+ const updatedPreList = instance.getData() || [];
92
+ const _current = _objectSpread(_objectSpread({}, cur), prev && prev.id ? {
74
93
  prevId: prev.id
75
- });
76
- const conflictList = checkConflicts(preList, _current, Conflict.rule);
94
+ } : {});
95
+ const conflictList = checkConflicts(updatedPreList, _current, Conflict.rule);
77
96
  const isConflict = conflictList.length > 0;
78
- if (!isConflict && prev) {
79
- // 如果没有冲突说明可以更新,需要删除之前的项,防止之前项目一致存在影响正常判断冲突
80
- const preItem = preList.find(i => i.id === prev.id);
81
- preItem && instance.deleteData(preItem);
97
+ if (!isConflict) {
98
+ // 如果没有冲突,添加新数据
82
99
  instance.setData(_current);
100
+ } else if (preItem) {
101
+ // 如果有冲突,恢复旧数据
102
+ instance.setData(preItem);
83
103
  }
84
104
  return {
85
105
  isConflict,
@@ -18,42 +18,37 @@ export class ScheduleDataManager {
18
18
  return;
19
19
  }
20
20
 
21
- // 创建一个新的列表,而不是修改现有列表
22
- let newList = [...ScheduleDataManager.list];
23
- if (Array.isArray(value)) {
24
- value.forEach(item => {
25
- if (Array.isArray(item)) {
26
- item.forEach(_item => {
27
- if (!_item || !_item.id) {
28
- return;
29
- }
30
- // 如果跟之前id相同需要先删除
31
- newList = newList.filter(i => i.id !== _item.id);
32
- // 添加新项
33
- newList.push(_item);
34
- });
35
- } else {
36
- if (!item || !item.id) {
37
- return;
38
- }
39
- // 如果跟之前id相同需要先删除
40
- newList = newList.filter(i => i.id !== item.id);
41
- // 添加新项
42
- newList.push(item);
43
- }
44
- });
45
- } else {
46
- if (!value || !value.id) {
21
+ // 收集所有要处理的项目,并按照最后一次出现的顺序去重
22
+ const itemsMap = new Map();
23
+ const addItem = item => {
24
+ if (!item || !item.id) {
47
25
  return;
48
26
  }
49
- // 如果跟之前id相同需要先删除
50
- newList = newList.filter(i => i.id !== value.id);
51
- // 添加新项
52
- newList.push(value);
27
+ if (itemsMap.has(item.id)) {
28
+ itemsMap.delete(item.id);
29
+ }
30
+ itemsMap.set(item.id, item);
31
+ };
32
+ const collectItems = input => {
33
+ if (Array.isArray(input)) {
34
+ input.forEach(subItem => {
35
+ if (Array.isArray(subItem)) {
36
+ collectItems(subItem);
37
+ } else {
38
+ addItem(subItem);
39
+ }
40
+ });
41
+ } else {
42
+ addItem(input);
43
+ }
44
+ };
45
+ collectItems(value);
46
+ if (itemsMap.size === 0) {
47
+ return;
53
48
  }
54
-
55
- // 更新列表
56
- ScheduleDataManager.list = newList;
49
+ const idsToAdd = new Set(itemsMap.keys());
50
+ const filteredList = ScheduleDataManager.list.filter(item => !idsToAdd.has(item.id));
51
+ ScheduleDataManager.list = [...filteredList, ...itemsMap.values()];
57
52
  }
58
53
  clearData() {
59
54
  ScheduleDataManager.list = [];
@@ -1,20 +1,16 @@
1
- import { ERhythm } from '../../types/rhythms';
2
- interface IReturn {
3
- type: ERhythm;
4
- payload: any;
5
- }
6
- export declare const updateRhythmsMessage: (data: Record<string, any>) => IReturn;
7
- export declare const updateRhythms: (data: Record<string, any>) => IReturn;
8
- export declare const updateLocalMessage: (data: Record<string, any>) => IReturn;
9
- export declare const updateUI: (data: Record<string, any>) => IReturn;
10
- export declare const initSystemInfo: (data: Record<string, any>) => IReturn;
11
- export declare const updateDp: (data: Record<string, any>) => IReturn;
1
+ import { RhythmAction, RhythmMessage, TRhythmData, LocalMessage, IState } from '../../types/rhythms';
2
+ export declare const updateRhythmsMessage: (data: RhythmMessage) => RhythmAction;
3
+ export declare const updateRhythms: (data: TRhythmData[]) => RhythmAction;
4
+ export declare const updateLocalMessage: (data: LocalMessage) => RhythmAction;
5
+ export declare const updateUI: (data: Partial<IState>) => RhythmAction;
6
+ export declare const initSystemInfo: (data: any) => RhythmAction;
7
+ export declare const updateDp: (data: Record<string, any>) => RhythmAction;
12
8
  declare const _default: {
13
- updateLocalMessage: (data: Record<string, any>) => IReturn;
14
- updateRhythms: (data: Record<string, any>) => IReturn;
15
- updateRhythmsMessage: (data: Record<string, any>) => IReturn;
16
- updateUI: (data: Record<string, any>) => IReturn;
17
- initSystemInfo: (data: Record<string, any>) => IReturn;
18
- updateDp: (data: Record<string, any>) => IReturn;
9
+ updateLocalMessage: (data: LocalMessage) => RhythmAction;
10
+ updateRhythms: (data: TRhythmData[]) => RhythmAction;
11
+ updateRhythmsMessage: (data: RhythmMessage) => RhythmAction;
12
+ updateUI: (data: Partial<IState>) => RhythmAction;
13
+ initSystemInfo: (data: any) => RhythmAction;
14
+ updateDp: (data: Record<string, any>) => RhythmAction;
19
15
  };
20
16
  export default _default;
@@ -1,24 +1,3 @@
1
1
  /// <reference types="react" />
2
- import { IState } from '../../types/rhythms';
3
- export declare const useRhythmReducer: (init: IState) => [IState | {
4
- localMessage: Partial<IState> | undefined;
5
- rhythms: [] | import("../../types/rhythms").TRhythmData[];
6
- rhythmsMessage: import("../../types/rhythms").RhythmMessage;
7
- systemInfo: any;
8
- dpState: Record<string, any>;
9
- } | {
10
- rhythms: Partial<IState> | undefined;
11
- localMessage: import("../../types/rhythms").LocalMessage;
12
- rhythmsMessage: import("../../types/rhythms").RhythmMessage;
13
- systemInfo: any;
14
- dpState: Record<string, any>;
15
- } | {
16
- rhythmsMessage: Partial<IState> | undefined;
17
- localMessage: import("../../types/rhythms").LocalMessage;
18
- rhythms: [] | import("../../types/rhythms").TRhythmData[];
19
- systemInfo: any;
20
- dpState: Record<string, any>;
21
- }, import("react").Dispatch<{
22
- type: string;
23
- payload?: Partial<IState> | undefined;
24
- }>];
2
+ import { IState, RhythmAction } from '../../types/rhythms';
3
+ export declare const useRhythmReducer: (init: IState) => [IState, import("react").Dispatch<RhythmAction>];
@@ -6,28 +6,27 @@ import { ERhythm } from '../../types/rhythms';
6
6
  // 默认值
7
7
  export const useRhythmReducer = init => {
8
8
  const rhythmReducer = useCallback((state, action) => {
9
- const oldState = _objectSpread({}, state);
10
9
  switch (action.type) {
11
10
  case ERhythm.UPDATE_LOCAL_MESSAGE:
12
- return _objectSpread(_objectSpread({}, oldState), {}, {
11
+ return _objectSpread(_objectSpread({}, state), {}, {
13
12
  localMessage: action.payload
14
13
  });
15
14
  case ERhythm.UPDATE_RHYTHMS:
16
- return _objectSpread(_objectSpread({}, oldState), {}, {
15
+ return _objectSpread(_objectSpread({}, state), {}, {
17
16
  rhythms: action.payload
18
17
  });
19
18
  case ERhythm.UPDATE_DP:
20
- return _objectSpread(_objectSpread({}, oldState), {}, {
21
- dpState: _objectSpread(_objectSpread({}, oldState.dpState), action.payload)
19
+ return _objectSpread(_objectSpread({}, state), {}, {
20
+ dpState: _objectSpread(_objectSpread({}, state.dpState), action.payload)
22
21
  });
23
22
  case ERhythm.UPDATE_RHYTHMS_MESSAGE:
24
- return _objectSpread(_objectSpread({}, oldState), {}, {
23
+ return _objectSpread(_objectSpread({}, state), {}, {
25
24
  rhythmsMessage: action.payload
26
25
  });
27
26
  case ERhythm.UPDATE_UI:
28
- return _objectSpread(_objectSpread({}, oldState), action.payload);
27
+ return _objectSpread(_objectSpread({}, state), action.payload);
29
28
  case ERhythm.INIT_SYSTEM_INFO:
30
- return _objectSpread(_objectSpread({}, oldState), {}, {
29
+ return _objectSpread(_objectSpread({}, state), {}, {
31
30
  systemInfo: action.payload
32
31
  });
33
32
  default:
@@ -1,8 +1,11 @@
1
1
  import { Dispatch } from 'react';
2
2
  import { TTimerData } from '../../types';
3
+ type TTimerDataWithId = TTimerData & {
4
+ id?: string;
5
+ };
3
6
  type TTimerReducer = {
4
- commonFlatTimerList: TTimerData[];
5
- commonFoldTimerList: TTimerData[];
7
+ commonFlatTimerList: TTimerDataWithId[];
8
+ commonFoldTimerList: TTimerDataWithId[];
6
9
  randomList: any[];
7
10
  rhythmList: any[];
8
11
  sleepList: any[];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ const sleepParserStub = {
2
+ parser: jest.fn(),
3
+ formatter: jest.fn()
4
+ };
5
+ const wakeUpParserStub = {
6
+ parser: jest.fn(),
7
+ formatter: jest.fn()
8
+ };
9
+ jest.mock('../sleep', () => ({
10
+ getSleepParser: jest.fn(() => sleepParserStub)
11
+ }));
12
+ jest.mock('../wakeup', () => ({
13
+ getWakeUpParser: jest.fn(() => wakeUpParserStub)
14
+ }));
15
+ import { autoDispatchTransDpFun, randomParser, rhythmParser, rtcTimerParser, timerReportParser } from '../index';
16
+ import { scheduleDpCodes } from '../../config/dpCodes';
17
+ describe('autoDispatchTransDpFun', () => {
18
+ it('returns random parser for RANDOM_TIMING', () => {
19
+ expect(autoDispatchTransDpFun(scheduleDpCodes.RANDOM_TIMING)).toBe(randomParser);
20
+ });
21
+ it('returns rhythm parser for RHYTHM_MODE', () => {
22
+ expect(autoDispatchTransDpFun(scheduleDpCodes.RHYTHM_MODE)).toBe(rhythmParser);
23
+ });
24
+ it('returns sleep parser singleton for SLEEP_MODE', () => {
25
+ const first = autoDispatchTransDpFun(scheduleDpCodes.SLEEP_MODE);
26
+ const second = autoDispatchTransDpFun(scheduleDpCodes.SLEEP_MODE);
27
+ expect(first).toBe(sleepParserStub);
28
+ expect(second).toBe(sleepParserStub);
29
+ });
30
+ it('returns RTC parser for RTC_TIMER', () => {
31
+ expect(autoDispatchTransDpFun(scheduleDpCodes.RTC_TIMER)).toBe(rtcTimerParser);
32
+ });
33
+ it('returns timer report parser for TIMER_REPORT', () => {
34
+ expect(autoDispatchTransDpFun(scheduleDpCodes.TIMER_REPORT)).toBe(timerReportParser);
35
+ });
36
+ it('returns wake up parser singleton for WAKE_UP_MODE', () => {
37
+ const first = autoDispatchTransDpFun(scheduleDpCodes.WAKE_UP_MODE);
38
+ const second = autoDispatchTransDpFun(scheduleDpCodes.WAKE_UP_MODE);
39
+ expect(first).toBe(wakeUpParserStub);
40
+ expect(second).toBe(wakeUpParserStub);
41
+ });
42
+ it('returns null for unsupported dpCode', () => {
43
+ expect(autoDispatchTransDpFun('UNKNOWN_DP_CODE')).toBeNull();
44
+ });
45
+ });
@@ -1,26 +1,43 @@
1
+ import "core-js/modules/esnext.iterator.constructor.js";
2
+ import "core-js/modules/esnext.iterator.for-each.js";
1
3
  import { rhythmParser } from '../rhythms';
2
- // Mock ScheduleLogger
3
- jest.mock('../../utils/ScheduleLogger', () => ({
4
- scheduleLogger: {
4
+ import { scheduleDpCodes } from '../../config/dpCodes';
5
+ jest.mock('../../utils/ScheduleLogger', () => {
6
+ const logger = {
5
7
  debug: jest.fn(),
6
8
  warn: jest.fn(),
7
9
  error: jest.fn()
8
- }
9
- }));
10
-
11
- // Mock getSupportIns
12
- const mockSupport = function () {
13
- let isSigMesh = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
14
- let isSupportTemp = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
10
+ };
15
11
  return {
16
- isSigMeshDevice: () => isSigMesh,
17
- isSupportDp: () => isSupportTemp
12
+ scheduleLogger: logger
18
13
  };
19
- };
14
+ });
20
15
  jest.mock('../../hooks/useCommonSupport', () => ({
21
- getSupportIns: jest.fn(() => mockSupport(false, true))
16
+ getSupportIns: jest.fn()
22
17
  }));
18
+ const {
19
+ scheduleLogger
20
+ } = require('../../utils/ScheduleLogger');
21
+ const {
22
+ getSupportIns
23
+ } = require('../../hooks/useCommonSupport');
23
24
  describe('RhythmParser', () => {
25
+ const createSupport = function () {
26
+ let isSupportTemp = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
27
+ return {
28
+ isSigMeshDevice: jest.fn().mockReturnValue(false),
29
+ isSupportDp: jest.fn().mockImplementation(dp => {
30
+ if (dp === scheduleDpCodes.TEMPERATURE) {
31
+ return isSupportTemp;
32
+ }
33
+ return true;
34
+ })
35
+ };
36
+ };
37
+ beforeEach(() => {
38
+ getSupportIns.mockReturnValue(createSupport(true));
39
+ Object.values(scheduleLogger).forEach(fn => fn.mockClear());
40
+ });
24
41
  describe('parser', () => {
25
42
  it('should return default data for empty DP string', () => {
26
43
  const result = rhythmParser.parser('');
@@ -31,12 +48,71 @@ describe('RhythmParser', () => {
31
48
  expect(result).toHaveProperty('number');
32
49
  expect(result).toHaveProperty('rhythms');
33
50
  });
51
+ it('should return default data when support instance is missing', () => {
52
+ getSupportIns.mockReturnValueOnce(null);
53
+ const result = rhythmParser.parser('001122334455');
54
+ expect(result).toHaveProperty('version', 0);
55
+ expect(result).toHaveProperty('rhythms');
56
+ });
57
+ it('should handle parser errors and log them', () => {
58
+ const panelUtils = require('@ray-js/panel-sdk/lib/utils');
59
+ const stepSpy = jest.spyOn(panelUtils, 'generateDpStrStep').mockImplementation(() => {
60
+ throw new Error('step error');
61
+ });
62
+ const result = rhythmParser.parser('0123456789abcdef0123456789ab');
63
+ expect(result).toEqual({});
64
+ expect(scheduleLogger.error).toHaveBeenCalledWith('rhythmParser parser error', expect.any(Error));
65
+ stepSpy.mockRestore();
66
+ });
34
67
  });
35
68
  describe('formatter', () => {
36
69
  it('should return empty string if data is null', () => {
37
70
  const result = rhythmParser.formatter(null);
38
71
  expect(result).toBe('');
39
72
  });
73
+ it('should return empty string when support instance is missing', () => {
74
+ getSupportIns.mockReturnValueOnce(null);
75
+ const result = rhythmParser.formatter({
76
+ version: 1,
77
+ power: true,
78
+ mode: 2,
79
+ weeks: [1, 1, 1, 1, 1, 1, 1, 1],
80
+ number: 1,
81
+ rhythms: [{
82
+ power: true,
83
+ hour: 8,
84
+ minute: 30,
85
+ hue: 100,
86
+ saturation: 90,
87
+ value: 80,
88
+ brightness: 70,
89
+ temperature: 60
90
+ }]
91
+ });
92
+ expect(result).toBe('');
93
+ expect(scheduleLogger.error).toHaveBeenCalledWith('协议解析 rhythms formatter =====', '数据为空');
94
+ });
95
+ it('should force temperature to 100 when device does not support temperature DP', () => {
96
+ getSupportIns.mockReturnValueOnce(createSupport(false));
97
+ const result = rhythmParser.formatter({
98
+ version: 0,
99
+ power: true,
100
+ mode: 1,
101
+ weeks: [1, 1, 1, 1, 1, 1, 1, 1],
102
+ number: 1,
103
+ rhythms: [{
104
+ power: true,
105
+ hour: 6,
106
+ minute: 15,
107
+ hue: 20,
108
+ saturation: 1,
109
+ value: 2,
110
+ brightness: 3,
111
+ temperature: 4
112
+ }]
113
+ });
114
+ expect(result.slice(-2)).toBe('64');
115
+ });
40
116
  it('should format a valid TRhythmData object', () => {
41
117
  const result = rhythmParser.formatter({
42
118
  version: 0,