Skip to content

Commit c623eae

Browse files
committed
feat(web): add WebEventParser and tests for event modification detection
1 parent f7fc98f commit c623eae

File tree

2 files changed

+368
-0
lines changed

2 files changed

+368
-0
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { faker } from "@faker-js/faker";
2+
import { Priorities } from "@core/constants/core.constants";
3+
import { createWebEvent } from "../../__tests__/utils/event.util/test.event.util";
4+
import { WebEventParser, isEventDirty } from "./event.parser";
5+
6+
describe("WebEventParser", () => {
7+
it("should return false when draft and original events are identical", () => {
8+
const originalEvent = createWebEvent();
9+
const draftEvent = createWebEvent({
10+
title: originalEvent.title,
11+
description: originalEvent.description,
12+
startDate: originalEvent.startDate,
13+
endDate: originalEvent.endDate,
14+
priority: originalEvent.priority,
15+
recurrence: originalEvent.recurrence,
16+
});
17+
18+
const parser = new WebEventParser(draftEvent, originalEvent);
19+
expect(parser.isDirty()).toBe(false);
20+
});
21+
22+
it("should return true when title has changed", () => {
23+
const originalEvent = createWebEvent();
24+
const draftEvent = createWebEvent({
25+
title: "Different Title",
26+
description: originalEvent.description,
27+
startDate: originalEvent.startDate,
28+
endDate: originalEvent.endDate,
29+
priority: originalEvent.priority,
30+
recurrence: originalEvent.recurrence,
31+
});
32+
33+
const parser = new WebEventParser(draftEvent, originalEvent);
34+
expect(parser.isDirty()).toBe(true);
35+
});
36+
37+
it("should return true when description has changed", () => {
38+
const originalEvent = createWebEvent();
39+
const draftEvent = createWebEvent({
40+
title: originalEvent.title,
41+
description: "Different Description",
42+
startDate: originalEvent.startDate,
43+
endDate: originalEvent.endDate,
44+
priority: originalEvent.priority,
45+
recurrence: originalEvent.recurrence,
46+
});
47+
48+
const parser = new WebEventParser(draftEvent, originalEvent);
49+
expect(parser.isDirty()).toBe(true);
50+
});
51+
52+
it("should return true when startDate has changed", () => {
53+
const originalEvent = createWebEvent();
54+
const draftEvent = createWebEvent({
55+
title: originalEvent.title,
56+
description: originalEvent.description,
57+
startDate: faker.date.future().toISOString(),
58+
endDate: originalEvent.endDate,
59+
priority: originalEvent.priority,
60+
recurrence: originalEvent.recurrence,
61+
});
62+
63+
const parser = new WebEventParser(draftEvent, originalEvent);
64+
expect(parser.isDirty()).toBe(true);
65+
});
66+
67+
it("should return true when endDate has changed", () => {
68+
const originalEvent = createWebEvent();
69+
const draftEvent = createWebEvent({
70+
title: originalEvent.title,
71+
description: originalEvent.description,
72+
startDate: originalEvent.startDate,
73+
endDate: faker.date.future().toISOString(),
74+
priority: originalEvent.priority,
75+
recurrence: originalEvent.recurrence,
76+
});
77+
78+
const parser = new WebEventParser(draftEvent, originalEvent);
79+
expect(parser.isDirty()).toBe(true);
80+
});
81+
82+
it("should return true when priority has changed", () => {
83+
const originalEvent = createWebEvent({ priority: Priorities.WORK });
84+
const draftEvent = createWebEvent({
85+
title: originalEvent.title,
86+
description: originalEvent.description,
87+
startDate: originalEvent.startDate,
88+
endDate: originalEvent.endDate,
89+
priority: Priorities.SELF,
90+
recurrence: originalEvent.recurrence,
91+
});
92+
93+
const parser = new WebEventParser(draftEvent, originalEvent);
94+
expect(parser.isDirty()).toBe(true);
95+
});
96+
97+
it("should return true when recurrence is added to non-recurring event", () => {
98+
const originalEvent = createWebEvent({ recurrence: undefined });
99+
const draftEvent = createWebEvent({
100+
title: originalEvent.title,
101+
description: originalEvent.description,
102+
startDate: originalEvent.startDate,
103+
endDate: originalEvent.endDate,
104+
priority: originalEvent.priority,
105+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
106+
});
107+
108+
const parser = new WebEventParser(draftEvent, originalEvent);
109+
expect(parser.isDirty()).toBe(true);
110+
});
111+
112+
it("should return true when recurrence is removed from recurring event", () => {
113+
const originalEvent = createWebEvent({
114+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
115+
});
116+
const draftEvent = createWebEvent({
117+
title: originalEvent.title,
118+
description: originalEvent.description,
119+
startDate: originalEvent.startDate,
120+
endDate: originalEvent.endDate,
121+
priority: originalEvent.priority,
122+
recurrence: undefined,
123+
});
124+
125+
const parser = new WebEventParser(draftEvent, originalEvent);
126+
expect(parser.isDirty()).toBe(true);
127+
});
128+
129+
it("should return true when recurrence rules have changed", () => {
130+
const originalEvent = createWebEvent({
131+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
132+
});
133+
const draftEvent = createWebEvent({
134+
title: originalEvent.title,
135+
description: originalEvent.description,
136+
startDate: originalEvent.startDate,
137+
endDate: originalEvent.endDate,
138+
priority: originalEvent.priority,
139+
recurrence: { rule: ["RRULE:FREQ=DAILY"] },
140+
});
141+
142+
const parser = new WebEventParser(draftEvent, originalEvent);
143+
expect(parser.isDirty()).toBe(true);
144+
});
145+
146+
it("should return true when recurrence rules array length has changed", () => {
147+
const originalEvent = createWebEvent({
148+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
149+
});
150+
const draftEvent = createWebEvent({
151+
title: originalEvent.title,
152+
description: originalEvent.description,
153+
startDate: originalEvent.startDate,
154+
endDate: originalEvent.endDate,
155+
priority: originalEvent.priority,
156+
recurrence: { rule: ["RRULE:FREQ=WEEKLY", "RRULE:BYDAY=MO"] },
157+
});
158+
159+
const parser = new WebEventParser(draftEvent, originalEvent);
160+
expect(parser.isDirty()).toBe(true);
161+
});
162+
163+
it("should return true when dates change in recurring event", () => {
164+
const originalEvent = createWebEvent({
165+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] },
166+
startDate: "2024-01-01T10:00:00Z",
167+
endDate: "2024-01-01T11:00:00Z",
168+
});
169+
const draftEvent = createWebEvent({
170+
title: originalEvent.title,
171+
description: originalEvent.description,
172+
startDate: "2024-01-01T11:00:00Z", // Different start time
173+
endDate: "2024-01-01T12:00:00Z", // Different end time
174+
priority: originalEvent.priority,
175+
recurrence: { rule: ["RRULE:FREQ=WEEKLY"] }, // Same recurrence
176+
});
177+
178+
const parser = new WebEventParser(draftEvent, originalEvent);
179+
expect(parser.isDirty()).toBe(true);
180+
});
181+
182+
it("should return false when only non-tracked fields change", () => {
183+
const originalEvent = createWebEvent();
184+
const draftEvent = createWebEvent({
185+
title: originalEvent.title,
186+
description: originalEvent.description,
187+
startDate: originalEvent.startDate,
188+
endDate: originalEvent.endDate,
189+
priority: originalEvent.priority,
190+
recurrence: originalEvent.recurrence,
191+
user: "different-user", // This field is not tracked
192+
});
193+
194+
const parser = new WebEventParser(draftEvent, originalEvent);
195+
expect(parser.isDirty()).toBe(false);
196+
});
197+
198+
it("should handle undefined recurrence gracefully", () => {
199+
const originalEvent = createWebEvent({ recurrence: undefined });
200+
const draftEvent = createWebEvent({
201+
title: originalEvent.title,
202+
description: originalEvent.description,
203+
startDate: originalEvent.startDate,
204+
endDate: originalEvent.endDate,
205+
priority: originalEvent.priority,
206+
recurrence: undefined,
207+
});
208+
209+
const parser = new WebEventParser(draftEvent, originalEvent);
210+
expect(parser.isDirty()).toBe(false);
211+
});
212+
213+
it("should handle empty recurrence rules", () => {
214+
const originalEvent = createWebEvent({
215+
recurrence: { rule: [] },
216+
});
217+
const draftEvent = createWebEvent({
218+
title: originalEvent.title,
219+
description: originalEvent.description,
220+
startDate: originalEvent.startDate,
221+
endDate: originalEvent.endDate,
222+
priority: originalEvent.priority,
223+
recurrence: { rule: [] },
224+
});
225+
226+
const parser = new WebEventParser(draftEvent, originalEvent);
227+
expect(parser.isDirty()).toBe(false);
228+
});
229+
});
230+
231+
describe("isDraftDirty", () => {
232+
it("should work as a standalone function", () => {
233+
const originalEvent = createWebEvent();
234+
const draftEvent = createWebEvent({
235+
title: "Different Title",
236+
description: originalEvent.description,
237+
startDate: originalEvent.startDate,
238+
endDate: originalEvent.endDate,
239+
priority: originalEvent.priority,
240+
recurrence: originalEvent.recurrence,
241+
});
242+
243+
expect(isEventDirty(draftEvent, originalEvent)).toBe(true);
244+
});
245+
246+
it("should return false for identical events", () => {
247+
const originalEvent = createWebEvent();
248+
const draftEvent = createWebEvent({
249+
title: originalEvent.title,
250+
description: originalEvent.description,
251+
startDate: originalEvent.startDate,
252+
endDate: originalEvent.endDate,
253+
priority: originalEvent.priority,
254+
recurrence: originalEvent.recurrence,
255+
});
256+
257+
expect(isEventDirty(draftEvent, originalEvent)).toBe(false);
258+
});
259+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Schema_WebEvent } from "../types/web.event.types";
2+
3+
/**
4+
* Parser for determining if an event has been modified (is dirty)
5+
*/
6+
export class WebEventParser {
7+
private readonly curr: Schema_WebEvent;
8+
private readonly orig: Schema_WebEvent;
9+
10+
constructor(curr: Schema_WebEvent, orig: Schema_WebEvent) {
11+
this.curr = curr;
12+
this.orig = orig;
13+
}
14+
15+
/**
16+
* Public method to check if the event is dirty (has been modified)
17+
*/
18+
public isDirty(): boolean {
19+
// Compare relevant fields that can change in the form
20+
const fieldsToCompare = [
21+
"title",
22+
"description",
23+
"startDate",
24+
"endDate",
25+
"priority",
26+
"recurrence",
27+
] as const;
28+
29+
return fieldsToCompare.some((field) => {
30+
const current = this.curr[field];
31+
const original = this.orig[field];
32+
const isRecurrenceField = field === "recurrence";
33+
34+
return isRecurrenceField
35+
? this.isRecurrenceChanged()
36+
: current !== original;
37+
});
38+
}
39+
40+
/**
41+
* Private method to check if recurrence has changed
42+
*/
43+
private isRecurrenceChanged(): boolean {
44+
if (!this.orig) return false;
45+
46+
const isDateChanged = this.isDateChanged();
47+
const oldRecurrence = this.orig?.recurrence?.rule ?? [];
48+
const newRecurrence = this.curr?.recurrence?.rule ?? [];
49+
50+
// Check if recurrence existence has changed
51+
const oldHasRecurrence =
52+
Array.isArray(oldRecurrence) && oldRecurrence.length > 0;
53+
const newHasRecurrence =
54+
Array.isArray(newRecurrence) && newRecurrence.length > 0;
55+
56+
if (oldHasRecurrence !== newHasRecurrence) {
57+
return true;
58+
}
59+
60+
// If both have recurrence, compare the rules
61+
if (oldHasRecurrence && newHasRecurrence) {
62+
// First check if the arrays are different in length or content
63+
if (oldRecurrence.length !== newRecurrence.length) {
64+
return true;
65+
}
66+
67+
// Check if any rule is different
68+
for (let i = 0; i < oldRecurrence.length; i++) {
69+
if (oldRecurrence[i] !== newRecurrence[i]) {
70+
return true;
71+
}
72+
}
73+
74+
// Also check for date changes
75+
return isDateChanged;
76+
}
77+
78+
// If neither has recurrence, only check date changes
79+
return isDateChanged;
80+
}
81+
82+
/**
83+
* Private method to check if the event has recurrence rules
84+
*/
85+
private isRecurrence(): boolean {
86+
const hasRRule = Array.isArray(this.curr?.recurrence?.rule);
87+
return hasRRule;
88+
}
89+
90+
/**
91+
* Private method to check if start or end dates have changed
92+
*/
93+
private isDateChanged(): boolean {
94+
const oldStartDate = this.orig?.startDate;
95+
const newStartDate = this.curr?.startDate;
96+
const oldEndDate = this.orig?.endDate;
97+
const newEndDate = this.curr?.endDate;
98+
99+
return oldStartDate !== newStartDate || oldEndDate !== newEndDate;
100+
}
101+
}
102+
103+
export const isEventDirty = (
104+
curr: Schema_WebEvent,
105+
orig: Schema_WebEvent,
106+
): boolean => {
107+
const parser = new WebEventParser(curr, orig);
108+
return parser.isDirty();
109+
};

0 commit comments

Comments
 (0)