@craftguild/jscalendar 0.5.2 → 0.5.4

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 (124) hide show
  1. package/README.md +130 -94
  2. package/dist/index.cjs +3923 -0
  3. package/dist/index.d.cts +802 -0
  4. package/dist/index.d.mts +802 -0
  5. package/dist/index.mjs +3927 -0
  6. package/package.json +46 -31
  7. package/dist/__tests__/builders.test.d.ts +0 -1
  8. package/dist/__tests__/builders.test.js +0 -101
  9. package/dist/__tests__/calendar-extra.test.d.ts +0 -1
  10. package/dist/__tests__/calendar-extra.test.js +0 -221
  11. package/dist/__tests__/calendar.test.d.ts +0 -1
  12. package/dist/__tests__/calendar.test.js +0 -97
  13. package/dist/__tests__/ical-extra.test.d.ts +0 -1
  14. package/dist/__tests__/ical-extra.test.js +0 -87
  15. package/dist/__tests__/ical.test.d.ts +0 -1
  16. package/dist/__tests__/ical.test.js +0 -72
  17. package/dist/__tests__/index.test.d.ts +0 -1
  18. package/dist/__tests__/index.test.js +0 -9
  19. package/dist/__tests__/patch.test.d.ts +0 -1
  20. package/dist/__tests__/patch.test.js +0 -47
  21. package/dist/__tests__/recurrence.test.d.ts +0 -1
  22. package/dist/__tests__/recurrence.test.js +0 -640
  23. package/dist/__tests__/search.test.d.ts +0 -1
  24. package/dist/__tests__/search.test.js +0 -264
  25. package/dist/__tests__/timezones.test.d.ts +0 -1
  26. package/dist/__tests__/timezones.test.js +0 -12
  27. package/dist/__tests__/utils.test.d.ts +0 -1
  28. package/dist/__tests__/utils.test.js +0 -127
  29. package/dist/__tests__/validation.test.d.ts +0 -1
  30. package/dist/__tests__/validation.test.js +0 -224
  31. package/dist/ical.d.ts +0 -13
  32. package/dist/ical.js +0 -270
  33. package/dist/index.d.ts +0 -3
  34. package/dist/index.js +0 -2
  35. package/dist/jscal/base.d.ts +0 -89
  36. package/dist/jscal/base.js +0 -173
  37. package/dist/jscal/builders.d.ts +0 -183
  38. package/dist/jscal/builders.js +0 -287
  39. package/dist/jscal/constants.d.ts +0 -11
  40. package/dist/jscal/constants.js +0 -11
  41. package/dist/jscal/datetime.d.ts +0 -14
  42. package/dist/jscal/datetime.js +0 -42
  43. package/dist/jscal/defaults.d.ts +0 -31
  44. package/dist/jscal/defaults.js +0 -102
  45. package/dist/jscal/duration.d.ts +0 -43
  46. package/dist/jscal/duration.js +0 -75
  47. package/dist/jscal/event.d.ts +0 -23
  48. package/dist/jscal/event.js +0 -78
  49. package/dist/jscal/group.d.ts +0 -31
  50. package/dist/jscal/group.js +0 -69
  51. package/dist/jscal/guards.d.ts +0 -19
  52. package/dist/jscal/guards.js +0 -25
  53. package/dist/jscal/ids.d.ts +0 -11
  54. package/dist/jscal/ids.js +0 -77
  55. package/dist/jscal/normalize.d.ts +0 -32
  56. package/dist/jscal/normalize.js +0 -45
  57. package/dist/jscal/task.d.ts +0 -23
  58. package/dist/jscal/task.js +0 -67
  59. package/dist/jscal/types.d.ts +0 -38
  60. package/dist/jscal/types.js +0 -1
  61. package/dist/jscal.d.ts +0 -145
  62. package/dist/jscal.js +0 -126
  63. package/dist/patch.d.ts +0 -18
  64. package/dist/patch.js +0 -216
  65. package/dist/recurrence/constants.d.ts +0 -13
  66. package/dist/recurrence/constants.js +0 -13
  67. package/dist/recurrence/date-utils.d.ts +0 -125
  68. package/dist/recurrence/date-utils.js +0 -259
  69. package/dist/recurrence/expand.d.ts +0 -23
  70. package/dist/recurrence/expand.js +0 -315
  71. package/dist/recurrence/rule-candidates.d.ts +0 -21
  72. package/dist/recurrence/rule-candidates.js +0 -120
  73. package/dist/recurrence/rule-generate.d.ts +0 -11
  74. package/dist/recurrence/rule-generate.js +0 -36
  75. package/dist/recurrence/rule-matchers.d.ts +0 -34
  76. package/dist/recurrence/rule-matchers.js +0 -120
  77. package/dist/recurrence/rule-normalize.d.ts +0 -9
  78. package/dist/recurrence/rule-normalize.js +0 -57
  79. package/dist/recurrence/rule-selectors.d.ts +0 -7
  80. package/dist/recurrence/rule-selectors.js +0 -21
  81. package/dist/recurrence/rules.d.ts +0 -14
  82. package/dist/recurrence/rules.js +0 -57
  83. package/dist/recurrence/types.d.ts +0 -27
  84. package/dist/recurrence/types.js +0 -1
  85. package/dist/recurrence.d.ts +0 -2
  86. package/dist/recurrence.js +0 -1
  87. package/dist/search.d.ts +0 -44
  88. package/dist/search.js +0 -292
  89. package/dist/timezones/chunk_1.d.ts +0 -2
  90. package/dist/timezones/chunk_1.js +0 -72
  91. package/dist/timezones/chunk_2.d.ts +0 -2
  92. package/dist/timezones/chunk_2.js +0 -72
  93. package/dist/timezones/chunk_3.d.ts +0 -2
  94. package/dist/timezones/chunk_3.js +0 -72
  95. package/dist/timezones/chunk_4.d.ts +0 -2
  96. package/dist/timezones/chunk_4.js +0 -72
  97. package/dist/timezones/chunk_5.d.ts +0 -2
  98. package/dist/timezones/chunk_5.js +0 -72
  99. package/dist/timezones/chunk_6.d.ts +0 -2
  100. package/dist/timezones/chunk_6.js +0 -72
  101. package/dist/timezones/chunk_7.d.ts +0 -2
  102. package/dist/timezones/chunk_7.js +0 -6
  103. package/dist/timezones.d.ts +0 -9
  104. package/dist/timezones.js +0 -452
  105. package/dist/types.d.ts +0 -246
  106. package/dist/types.js +0 -1
  107. package/dist/utils.d.ts +0 -82
  108. package/dist/utils.js +0 -164
  109. package/dist/validate/asserts.d.ts +0 -155
  110. package/dist/validate/asserts.js +0 -381
  111. package/dist/validate/constants.d.ts +0 -25
  112. package/dist/validate/constants.js +0 -33
  113. package/dist/validate/error.d.ts +0 -19
  114. package/dist/validate/error.js +0 -25
  115. package/dist/validate/validators-common.d.ts +0 -64
  116. package/dist/validate/validators-common.js +0 -390
  117. package/dist/validate/validators-objects.d.ts +0 -8
  118. package/dist/validate/validators-objects.js +0 -70
  119. package/dist/validate/validators-recurrence.d.ts +0 -15
  120. package/dist/validate/validators-recurrence.js +0 -115
  121. package/dist/validate/validators.d.ts +0 -1
  122. package/dist/validate/validators.js +0 -1
  123. package/dist/validate.d.ts +0 -2
  124. package/dist/validate.js +0 -2
package/package.json CHANGED
@@ -1,34 +1,49 @@
1
1
  {
2
- "name": "@craftguild/jscalendar",
3
- "version": "0.5.2",
4
- "description": "RFC 8984 (JSCalendar) data model helpers for TypeScript",
5
- "license": "MIT",
6
- "type": "module",
7
- "main": "dist/index.js",
8
- "types": "dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "default": "./dist/index.js"
2
+ "name": "@craftguild/jscalendar",
3
+ "version": "0.5.4",
4
+ "description": "RFC 8984 (JSCalendar) data model helpers for TypeScript",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.cjs",
8
+ "module": "dist/index.mjs",
9
+ "types": "dist/index.d.mts",
10
+ "exports": {
11
+ ".": {
12
+ "types": {
13
+ "import": "./dist/index.d.mts",
14
+ "require": "./dist/index.d.cts"
15
+ },
16
+ "import": "./dist/index.mjs",
17
+ "require": "./dist/index.cjs",
18
+ "default": "./dist/index.mjs"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "prepublish": "tsdown",
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:coverage": "vitest run --coverage",
30
+ "prepare": "husky"
31
+ },
32
+ "devDependencies": {
33
+ "@vitest/coverage-v8": "^2.1.9",
34
+ "husky": "^9.1.7",
35
+ "lint-staged": "^16.2.7",
36
+ "prettier": "3.8.1",
37
+ "tsdown": "^0.20.1",
38
+ "tsx": "^4.21.0",
39
+ "typescript": "^5.6.2",
40
+ "vitest": "^2.1.9"
41
+ },
42
+ "dependencies": {
43
+ "date-fns": "^4.1.0",
44
+ "date-fns-tz": "^3.2.0"
45
+ },
46
+ "lint-staged": {
47
+ "**/*": "prettier --write --ignore-unknown"
13
48
  }
14
- },
15
- "files": [
16
- "dist"
17
- ],
18
- "scripts": {
19
- "build": "tsc -p tsconfig.json",
20
- "test": "vitest run",
21
- "test:watch": "vitest",
22
- "test:coverage": "vitest run --coverage"
23
- },
24
- "devDependencies": {
25
- "@vitest/coverage-v8": "^2.1.9",
26
- "tsx": "^4.21.0",
27
- "typescript": "^5.6.2",
28
- "vitest": "^2.1.9"
29
- },
30
- "dependencies": {
31
- "date-fns": "^4.1.0",
32
- "date-fns-tz": "^3.2.0"
33
- }
34
49
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,101 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { JsCal } from "../jscal.js";
3
- import { buildAbsoluteTrigger, buildAlert, buildIdMap, buildLink, buildLocation, buildNDay, buildOffsetTrigger, buildRecurrenceRule, buildRelation, buildTimeZone, buildTimeZoneMap, buildTimeZoneRule, buildVirtualLocation, } from "../jscal/builders.js";
4
- describe("builders", () => {
5
- it("builds strict objects with @type", () => {
6
- const participant = JsCal.Participant({ name: "Alice", roles: { attendee: true } });
7
- const location = buildLocation({ name: "Room A" });
8
- const vloc = buildVirtualLocation({ name: "Zoom", uri: "https://example.com" });
9
- const link = buildLink({ href: "https://example.com" });
10
- const relation = buildRelation({ relation: { parent: true } });
11
- const alert = buildAlert({ trigger: buildOffsetTrigger({ offset: JsCal.duration.minutes(-15) }) });
12
- const absoluteTrigger = buildAbsoluteTrigger({ when: "2026-02-01T00:00:00Z" });
13
- const nday = buildNDay({ day: "mo" });
14
- const rule = buildRecurrenceRule({ frequency: "daily" });
15
- const tzRule = buildTimeZoneRule({
16
- start: "2026-01-01T00:00:00",
17
- offsetFrom: "+09:00",
18
- offsetTo: "+09:00",
19
- });
20
- const tz = buildTimeZone({
21
- tzId: "Asia/Tokyo",
22
- standard: [tzRule],
23
- });
24
- expect(participant["@type"]).toBe("Participant");
25
- expect(location["@type"]).toBe("Location");
26
- expect(vloc["@type"]).toBe("VirtualLocation");
27
- expect(link["@type"]).toBe("Link");
28
- expect(relation["@type"]).toBe("Relation");
29
- expect(alert["@type"]).toBe("Alert");
30
- expect(absoluteTrigger["@type"]).toBe("AbsoluteTrigger");
31
- expect(nday["@type"]).toBe("NDay");
32
- expect(rule["@type"]).toBe("RecurrenceRule");
33
- expect(tz["@type"]).toBe("TimeZone");
34
- });
35
- it("builds id maps with generated ids", () => {
36
- const participants = JsCal.participants([
37
- { value: { name: "Alice", roles: { attendee: true } } },
38
- { value: { name: "Bob", roles: { attendee: true } } },
39
- ]);
40
- expect(Object.keys(participants).length).toBe(2);
41
- const locations = JsCal.locations([{ value: { name: "Room A" } }]);
42
- expect(Object.keys(locations).length).toBe(1);
43
- const links = JsCal.links([{ value: { href: "https://example.com" } }]);
44
- expect(Object.keys(links).length).toBe(1);
45
- const related = JsCal.relatedTo([{ value: { relation: { parent: true } } }]);
46
- expect(Object.keys(related).length).toBe(1);
47
- });
48
- it("merges id maps with explicit ids", () => {
49
- const existing = JsCal.locations([{ id: "loc-1", value: { name: "Room A" } }]);
50
- const merged = JsCal.locations([
51
- { id: "loc-1", value: { name: "Room A Updated" } },
52
- { value: { name: "Room B" } },
53
- ], existing);
54
- expect(existing["loc-1"]?.name).toBe("Room A");
55
- expect(merged["loc-1"]?.name).toBe("Room A Updated");
56
- expect(Object.keys(merged).length).toBe(2);
57
- });
58
- it("rejects wrapper inputs that mix value and direct fields", () => {
59
- expect(() => JsCal.locations([{ name: "Room A" }])).toThrowError();
60
- });
61
- it("builds a time zone map keyed by tzId", () => {
62
- const map = buildTimeZoneMap([
63
- { tzId: "Asia/Tokyo", standard: [{ "@type": "TimeZoneRule", start: "2026-01-01T00:00:00", offsetFrom: "+09:00", offsetTo: "+09:00" }] },
64
- ]);
65
- expect(map["Asia/Tokyo"]?.tzId).toBe("Asia/Tokyo");
66
- });
67
- it("buildIdMap uses a custom id function", () => {
68
- const map = buildIdMap([{ value: { name: "A" } }], (item) => item, (_item, index) => `id-${index}`);
69
- expect(Object.keys(map)).toEqual(["id-0"]);
70
- });
71
- it("throws when @type mismatches", () => {
72
- // Intentionally bypass type safety to assert @type validation errors.
73
- const bad = { "@type": "Location" };
74
- expect(() => JsCal.Participant(bad)).toThrowError("participant: must have @type Participant");
75
- });
76
- it("throws for mismatched @type across builders", () => {
77
- // Intentionally bypass type safety to assert @type validation errors.
78
- expect(() => buildLocation({ "@type": "Participant" }))
79
- .toThrowError("location: must have @type Location");
80
- expect(() => buildVirtualLocation({ "@type": "Location" }))
81
- .toThrowError("virtualLocation: must have @type VirtualLocation");
82
- expect(() => buildAlert({ "@type": "Link" }))
83
- .toThrowError("alert: must have @type Alert");
84
- expect(() => buildOffsetTrigger({ "@type": "Alert" }))
85
- .toThrowError("offsetTrigger: must have @type OffsetTrigger");
86
- expect(() => buildAbsoluteTrigger({ "@type": "Alert" }))
87
- .toThrowError("absoluteTrigger: must have @type AbsoluteTrigger");
88
- expect(() => buildRelation({ "@type": "Link" }))
89
- .toThrowError("relation: must have @type Relation");
90
- expect(() => buildLink({ "@type": "Relation" }))
91
- .toThrowError("link: must have @type Link");
92
- expect(() => buildTimeZone({ "@type": "Alert" }))
93
- .toThrowError("timeZone: must have @type TimeZone");
94
- expect(() => buildTimeZoneRule({ "@type": "TimeZone" }))
95
- .toThrowError("timeZoneRule: must have @type TimeZoneRule");
96
- expect(() => buildRecurrenceRule({ "@type": "NDay" }))
97
- .toThrowError("recurrenceRule: must have @type RecurrenceRule");
98
- expect(() => buildNDay({ "@type": "RecurrenceRule" }))
99
- .toThrowError("nday: must have @type NDay");
100
- });
101
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,221 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { JsCal, createId, isEvent, isGroup, isTask } from "../jscal.js";
3
- const fixedNow = () => "2026-02-01T00:00:00Z";
4
- function makeEvent() {
5
- return new JsCal.Event({
6
- title: "Kickoff",
7
- start: "2026-02-02T10:00:00",
8
- }, { now: fixedNow });
9
- }
10
- describe("JsCal helpers", () => {
11
- it("creates group and adds entry", () => {
12
- const group = new JsCal.Group({ entries: [] }, { now: fixedNow });
13
- const event = makeEvent();
14
- const nextGroup = group.addEntry(event.data);
15
- expect(group.data.entries.length).toBe(0);
16
- expect(nextGroup.data.entries.length).toBe(1);
17
- });
18
- it("accepts JsCal instances and ejected entries in groups", () => {
19
- const event = makeEvent();
20
- const task = new JsCal.Task({ title: "Task", start: "2026-02-02T10:00:00" }, { now: fixedNow });
21
- const group = new JsCal.Group({
22
- entries: [event, task.eject()],
23
- }, { now: fixedNow });
24
- expect(group.data.entries.length).toBe(2);
25
- expect(group.data.entries[0]?.["@type"]).toBe("Event");
26
- expect(group.data.entries[1]?.["@type"]).toBe("Task");
27
- });
28
- it("does not expose addEntry on non-group", () => {
29
- const event = makeEvent();
30
- expect("addEntry" in event).toBe(false);
31
- });
32
- it("adds locations and virtual locations", () => {
33
- const event = makeEvent();
34
- const locId = "loc-1";
35
- const vlocId = "vloc-1";
36
- const withLocation = event.addLocation({ name: "Room A" }, locId);
37
- const withVirtualLocation = withLocation.addVirtualLocation({ name: "Zoom", uri: "https://example.com" }, vlocId);
38
- const locations = withVirtualLocation.data.locations;
39
- const virtualLocations = withVirtualLocation.data.virtualLocations;
40
- expect(locations).toBeDefined();
41
- expect(virtualLocations).toBeDefined();
42
- if (!locations || !virtualLocations)
43
- throw new Error("Missing locations");
44
- expect(locations[locId]?.name).toBe("Room A");
45
- expect(virtualLocations[vlocId]?.uri).toBe("https://example.com");
46
- });
47
- it("adds participants and alerts", () => {
48
- const event = makeEvent();
49
- const participantId = "p1";
50
- const alertId = "a1";
51
- const withParticipant = event.addParticipant({ roles: { attendee: true }, email: "a@example.com" }, participantId);
52
- const withAlert = withParticipant.addAlert({ trigger: { "@type": "AbsoluteTrigger", when: "2026-02-01T01:00:00Z" } }, alertId);
53
- const participants = withAlert.data.participants;
54
- const alerts = withAlert.data.alerts;
55
- expect(participants).toBeDefined();
56
- expect(alerts).toBeDefined();
57
- if (!participants || !alerts)
58
- throw new Error("Missing participants/alerts");
59
- expect(participants[participantId]?.email).toBe("a@example.com");
60
- expect(alerts[alertId]?.trigger).toBeTruthy();
61
- expect(participants[participantId]?.participationStatus).toBe("needs-action");
62
- expect(participants[participantId]?.expectReply).toBe(false);
63
- expect(participants[participantId]?.scheduleAgent).toBe("server");
64
- expect(participants[participantId]?.scheduleForceSend).toBe(false);
65
- expect(participants[participantId]?.scheduleSequence).toBe(0);
66
- expect(alerts[alertId]?.action).toBe("display");
67
- });
68
- it("defaults relativeTo for offset triggers", () => {
69
- const event = makeEvent();
70
- const alertId = "a1";
71
- const withAlert = event.addAlert({ trigger: { "@type": "OffsetTrigger", offset: "PT15M" } }, alertId);
72
- const alerts = withAlert.data.alerts;
73
- expect(alerts).toBeDefined();
74
- if (!alerts)
75
- throw new Error("Missing alerts");
76
- const trigger = alerts[alertId]?.trigger;
77
- if (!trigger || trigger["@type"] !== "OffsetTrigger")
78
- throw new Error("Missing trigger");
79
- expect(trigger.relativeTo).toBe("start");
80
- });
81
- it("updates sequence for alerts but not for participants", () => {
82
- const event = makeEvent();
83
- const initialSequence = event.data.sequence ?? 0;
84
- const withParticipant = event.addParticipant({ roles: { attendee: true }, email: "a@example.com" }, "p1");
85
- expect(withParticipant.data.sequence ?? 0).toBe(initialSequence);
86
- const withAlert = withParticipant.addAlert({ trigger: { "@type": "AbsoluteTrigger", when: "2026-02-01T01:00:00Z" } }, "a1");
87
- expect(withAlert.data.sequence ?? 0).toBe(initialSequence + 1);
88
- });
89
- it("clones and eject return deep copies", () => {
90
- const event = makeEvent();
91
- const clone = event.clone();
92
- clone.data.title = "Changed";
93
- expect(event.data.title).toBe("Kickoff");
94
- const json = event.eject();
95
- json.title = "Changed";
96
- expect(event.data.title).toBe("Kickoff");
97
- });
98
- it("builds participant inputs without @type", () => {
99
- const participant = JsCal.Participant({ name: "Alice", roles: { attendee: true } });
100
- const task = new JsCal.Task({
101
- start: "2026-02-02T10:00:00",
102
- participants: { p1: participant },
103
- });
104
- expect(task.data.participants?.p1?.["@type"]).toBe("Participant");
105
- });
106
- it("builds participant maps with stable ids", () => {
107
- const participants = JsCal.participants([
108
- { value: { name: "Alice", roles: { attendee: true } } },
109
- { value: { name: "Bob", roles: { attendee: true } } },
110
- ]);
111
- const task = new JsCal.Task({
112
- start: "2026-02-02T10:00:00",
113
- participants,
114
- });
115
- expect(Object.keys(task.data.participants ?? {}).length).toBe(2);
116
- });
117
- it("createId returns base64url without padding", () => {
118
- const id = createId();
119
- expect(id).toMatch(/^[A-Za-z0-9_-]+$/);
120
- expect(id).not.toContain("=");
121
- });
122
- it("supports task creation", () => {
123
- const task = new JsCal.Task({
124
- title: "Do work",
125
- start: "2026-02-02T10:00:00",
126
- }, { now: fixedNow });
127
- expect(task.data["@type"]).toBe("Task");
128
- expect(task.data.created).toBe("2026-02-01T00:00:00Z");
129
- });
130
- it("supports group creation", () => {
131
- const group = new JsCal.Group({ entries: [] }, { now: fixedNow });
132
- expect(group.data["@type"]).toBe("Group");
133
- expect(group.data.created).toBe("2026-02-01T00:00:00Z");
134
- });
135
- it("accepts Date inputs and null time zones", () => {
136
- const start = new Date("2026-02-02T10:00:00");
137
- const event = new JsCal.Event({ title: "Date input", start, timeZone: null }, { now: fixedNow });
138
- expect(event.data.start).toMatch(/^2026-02-02T/);
139
- expect(event.data.timeZone).toBeNull();
140
- const task = new JsCal.Task({ title: "No TZ", start, timeZone: null }, { now: fixedNow });
141
- expect(task.data.timeZone).toBeNull();
142
- });
143
- it("accepts Date inputs for updated/created", () => {
144
- const updated = new Date("2026-02-02T10:00:00Z");
145
- const created = new Date("2026-02-01T09:00:00Z");
146
- const event = new JsCal.Event({
147
- title: "Dates",
148
- start: "2026-02-02T10:00:00",
149
- updated,
150
- created,
151
- });
152
- expect(event.data.updated).toBe("2026-02-02T10:00:00Z");
153
- expect(event.data.created).toBe("2026-02-01T09:00:00Z");
154
- });
155
- it("rejects invalid event start", () => {
156
- expect(() => new JsCal.Event({ title: "Bad", start: "" })).toThrow();
157
- });
158
- it("resolves task time zones", () => {
159
- const task = new JsCal.Task({ title: "TZ", start: "2026-02-02T10:00:00", timeZone: "asia/tokyo" }, { now: fixedNow });
160
- expect(task.data.timeZone).toBe("Asia/Tokyo");
161
- });
162
- it("derives task progress from participant statuses", () => {
163
- const completed = new JsCal.Task({
164
- title: "Completed",
165
- participants: {
166
- p1: { "@type": "Participant", roles: { attendee: true }, progress: "completed" },
167
- },
168
- }, { now: fixedNow });
169
- expect(completed.data.progress).toBe("completed");
170
- const failed = new JsCal.Task({
171
- title: "Failed",
172
- participants: {
173
- p1: { "@type": "Participant", roles: { attendee: true }, progress: "failed" },
174
- },
175
- }, { now: fixedNow });
176
- expect(failed.data.progress).toBe("failed");
177
- const inProcess = new JsCal.Task({
178
- title: "In process",
179
- participants: {
180
- p1: { "@type": "Participant", roles: { attendee: true }, progress: "in-process" },
181
- },
182
- }, { now: fixedNow });
183
- expect(inProcess.data.progress).toBe("in-process");
184
- const fallback = new JsCal.Task({
185
- title: "Fallback",
186
- participants: {
187
- p1: { "@type": "Participant", roles: { attendee: true }, progress: "delegated" },
188
- },
189
- }, { now: fixedNow });
190
- expect(fallback.data.progress).toBe("needs-action");
191
- });
192
- it("clones task and group objects", () => {
193
- const task = new JsCal.Task({ title: "Task", start: "2026-02-02T10:00:00" }, { now: fixedNow });
194
- const taskClone = task.clone();
195
- taskClone.data.title = "Changed";
196
- expect(task.data.title).toBe("Task");
197
- const group = new JsCal.Group({ entries: [] }, { now: fixedNow });
198
- const groupClone = group.clone();
199
- groupClone.data.title = "Changed";
200
- expect(group.data.title).toBe("");
201
- });
202
- it("requires entries for group creation", () => {
203
- // @ts-expect-error entries are required
204
- expect(() => new JsCal.Group({})).toThrow();
205
- });
206
- it("supports get/set helpers", () => {
207
- const event = makeEvent();
208
- expect(event.get("title")).toBe("Kickoff");
209
- const updated = event.set("title", "Updated");
210
- expect(event.get("title")).toBe("Kickoff");
211
- expect(updated.get("title")).toBe("Updated");
212
- });
213
- it("exposes type guards", () => {
214
- const event = makeEvent().eject();
215
- const task = new JsCal.Task({ title: "Task", start: "2026-02-02T10:00:00" }).eject();
216
- const group = new JsCal.Group({ entries: [] }).eject();
217
- expect(isEvent(event)).toBe(true);
218
- expect(isTask(task)).toBe(true);
219
- expect(isGroup(group)).toBe(true);
220
- });
221
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,97 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { JsCal, createUid } from "../jscal.js";
3
- const fixedNow = () => "2026-02-01T00:00:00Z";
4
- function makeEvent() {
5
- return new JsCal.Event({
6
- title: "Kickoff",
7
- start: "2026-02-02T10:00:00",
8
- }, { now: fixedNow });
9
- }
10
- describe("JsCal.Event", () => {
11
- it("fills uid and updated", () => {
12
- const event = makeEvent();
13
- expect(event.data.uid).toBeTruthy();
14
- expect(event.data.updated).toBe("2026-02-01T00:00:00Z");
15
- expect(event.data["@type"]).toBe("Event");
16
- expect(event.data.sequence).toBe(0);
17
- expect(event.data.duration).toBe("PT0S");
18
- expect(event.data.status).toBe("confirmed");
19
- expect(event.data.created).toBe("2026-02-01T00:00:00Z");
20
- expect(event.data.descriptionContentType).toBe("text/plain");
21
- expect(event.data.showWithoutTime).toBe(false);
22
- expect(event.data.freeBusyStatus).toBe("busy");
23
- expect(event.data.privacy).toBe("public");
24
- expect(event.data.useDefaultAlerts).toBe(false);
25
- expect(event.data.excluded).toBe(false);
26
- expect(event.data.timeZone).toBeUndefined();
27
- });
28
- it("throws when required fields are missing", () => {
29
- expect(() => {
30
- // @ts-expect-error start is required
31
- new JsCal.Event({ title: "Missing start" }, { now: fixedNow });
32
- }).toThrow(/Event.start/);
33
- });
34
- it("accepts explicit @type", () => {
35
- const event = new JsCal.Event({ start: "2026-02-02T10:00:00" }, { now: fixedNow });
36
- expect(event.data["@type"]).toBe("Event");
37
- });
38
- it("accepts Date for start", () => {
39
- const event = new JsCal.Event({ start: new Date("2026-02-02T10:00:00") }, { now: fixedNow });
40
- expect(event.data.start).toBe("2026-02-02T10:00:00");
41
- });
42
- it("does not set timeZone when missing", () => {
43
- const event = new JsCal.Event({ start: new Date("2026-02-02T10:00:00") }, { now: fixedNow });
44
- expect(event.data.timeZone).toBeUndefined();
45
- });
46
- it("accepts duration in seconds", () => {
47
- const event = new JsCal.Event({ start: new Date("2026-02-02T10:00:00"), duration: 3600 }, { now: fixedNow });
48
- expect(event.data.duration).toBe("PT1H");
49
- });
50
- it("handles duration boundaries", () => {
51
- const zero = new JsCal.Event({ start: new Date("2026-02-02T10:00:00"), duration: 0 }, { now: fixedNow });
52
- expect(zero.data.duration).toBe("PT0S");
53
- const seconds = new JsCal.Event({ start: new Date("2026-02-02T10:00:00"), duration: 59 }, { now: fixedNow });
54
- expect(seconds.data.duration).toBe("PT59S");
55
- const negative = new JsCal.Event({ start: new Date("2026-02-02T10:00:00"), duration: -1 }, { now: fixedNow });
56
- expect(negative.data.duration).toBe("PT0S");
57
- });
58
- });
59
- describe("Event.patch", () => {
60
- it("increments sequence on non-participant patches", () => {
61
- const event = makeEvent();
62
- const patched = event.patch({ title: "Updated" }, { now: fixedNow });
63
- expect(patched.data.sequence).toBe(1);
64
- expect(event.data.sequence).toBe(0);
65
- });
66
- it("respects touch=false", () => {
67
- const event = makeEvent();
68
- const before = event.data.updated;
69
- const patched = event.patch({ title: "No touch" }, { touch: false, now: () => "2026-02-02T00:00:00Z" });
70
- expect(patched.data.updated).toBe(before);
71
- });
72
- it("applies patch and updates metadata", () => {
73
- const event = makeEvent();
74
- const patched = event.patch({ title: "Patched" }, { now: fixedNow });
75
- expect(patched.data.title).toBe("Patched");
76
- expect(patched.data.updated).toBe("2026-02-01T00:00:00Z");
77
- expect(patched.data.sequence).toBe(1);
78
- });
79
- it("does not increment sequence for participants-only patch", () => {
80
- const event = makeEvent();
81
- const patched = event.patch({
82
- participants: {
83
- p1: {
84
- "@type": "Participant",
85
- roles: { attendee: true },
86
- },
87
- },
88
- }, { now: fixedNow });
89
- expect(patched.data.sequence ?? 0).toBe(0);
90
- });
91
- });
92
- describe("createUid", () => {
93
- it("generates unique-ish id format", () => {
94
- const uid = createUid();
95
- expect(uid).toMatch(/^[0-9a-f-]{36}$/);
96
- });
97
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,87 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { JsCal } from "../jscal.js";
3
- const task = {
4
- "@type": "Task",
5
- uid: "t1",
6
- updated: "2026-02-01T00:00:00Z",
7
- title: "Write notes",
8
- description: "Prepare summary",
9
- sequence: 1,
10
- start: "2026-02-02T09:00:00",
11
- due: "2026-02-02T17:00:00",
12
- timeZone: "America/Los_Angeles",
13
- percentComplete: 50,
14
- progress: "in-process",
15
- };
16
- const group = {
17
- "@type": "Group",
18
- uid: "g1",
19
- updated: "2026-02-01T00:00:00Z",
20
- title: "Group",
21
- entries: [task],
22
- };
23
- const event = {
24
- "@type": "Event",
25
- uid: "e2",
26
- updated: "2026-02-01T00:00:00Z",
27
- title: "Planning",
28
- start: "2026-02-03T09:00:00",
29
- };
30
- const taskWithMethod = {
31
- "@type": "Task",
32
- uid: "t2",
33
- updated: "2026-02-01T00:00:00Z",
34
- title: "Method test",
35
- method: "request",
36
- };
37
- const taskWithoutTimeZone = {
38
- "@type": "Task",
39
- uid: "t3",
40
- updated: "2026-02-01T00:00:00Z",
41
- title: "No TZ",
42
- start: "2026-02-05T09:00:00",
43
- due: "2026-02-05T17:00:00",
44
- };
45
- describe("toICal extras", () => {
46
- it("exports VTODO with key fields", () => {
47
- const ical = JsCal.toICal([task], { includeXJSCalendar: false });
48
- expect(ical).toContain("BEGIN:VTODO");
49
- expect(ical).toContain("UID:t1");
50
- expect(ical).toContain("DTSTART;TZID=America/Los_Angeles:20260202T090000");
51
- expect(ical).toContain("DUE;TZID=America/Los_Angeles:20260202T170000");
52
- expect(ical).toContain("PERCENT-COMPLETE:50");
53
- expect(ical).toContain("STATUS:IN-PROCESS");
54
- });
55
- it("exports group as VCALENDAR with entries", () => {
56
- const ical = JsCal.toICal([group], { includeXJSCalendar: true });
57
- expect(ical).toContain("BEGIN:VCALENDAR");
58
- expect(ical).toContain("X-JSCALENDAR-GROUP:");
59
- expect(ical).toContain("BEGIN:VTODO");
60
- });
61
- it("exports mixed arrays (group + event + task)", () => {
62
- const ical = JsCal.toICal([group, event, task]);
63
- expect(ical).toContain("BEGIN:VEVENT");
64
- expect(ical).toContain("BEGIN:VTODO");
65
- });
66
- it("uses method from first object that defines it", () => {
67
- const ical = JsCal.toICal([event, taskWithMethod]);
68
- expect(ical).toContain("METHOD:REQUEST");
69
- });
70
- it("exports VTODO without time zone parameters", () => {
71
- const ical = JsCal.toICal([taskWithoutTimeZone], { includeXJSCalendar: false });
72
- expect(ical).toContain("DTSTART:20260205T090000");
73
- expect(ical).toContain("DUE:20260205T170000");
74
- });
75
- it("ignores unknown object types", () => {
76
- const unknownObject = {
77
- "@type": "Unknown",
78
- uid: "x1",
79
- updated: "2026-02-01T00:00:00Z",
80
- };
81
- // @ts-expect-error unknown object type
82
- const ical = JsCal.toICal([unknownObject], { includeXJSCalendar: false });
83
- expect(ical).toContain("BEGIN:VCALENDAR");
84
- expect(ical).not.toContain("BEGIN:VEVENT");
85
- expect(ical).not.toContain("BEGIN:VTODO");
86
- });
87
- });
@@ -1 +0,0 @@
1
- export {};