@f-o-t/datetime 0.1.0

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.
@@ -0,0 +1,173 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { DateTime } from "../../core/datetime";
3
+ import { formatPlugin } from "./index";
4
+
5
+ describe("Format Plugin", () => {
6
+ // Install plugin before running tests
7
+ DateTime.extend(formatPlugin);
8
+
9
+ describe("format()", () => {
10
+ it("should format year tokens", () => {
11
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
12
+ expect((dt as any).format("YYYY")).toBe("2024");
13
+ expect((dt as any).format("YY")).toBe("24");
14
+ });
15
+
16
+ it("should format month tokens", () => {
17
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
18
+ expect((dt as any).format("MM")).toBe("01");
19
+ expect((dt as any).format("M")).toBe("1");
20
+ });
21
+
22
+ it("should format day tokens", () => {
23
+ const dt = new DateTime("2024-01-05T14:30:45.123Z");
24
+ expect((dt as any).format("DD")).toBe("05");
25
+ expect((dt as any).format("D")).toBe("5");
26
+ });
27
+
28
+ it("should format hour tokens (24-hour)", () => {
29
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
30
+ expect((dt as any).format("HH")).toBe("14");
31
+ expect((dt as any).format("H")).toBe("14");
32
+ });
33
+
34
+ it("should format hour tokens (12-hour)", () => {
35
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
36
+ expect((dt as any).format("hh")).toBe("02");
37
+ expect((dt as any).format("h")).toBe("2");
38
+
39
+ const dtAM = new DateTime("2024-01-15T09:30:45.123Z");
40
+ expect((dtAM as any).format("hh")).toBe("09");
41
+ expect((dtAM as any).format("h")).toBe("9");
42
+ });
43
+
44
+ it("should handle midnight and noon in 12-hour format", () => {
45
+ const midnight = new DateTime("2024-01-15T00:00:00.000Z");
46
+ expect((midnight as any).format("h")).toBe("12");
47
+
48
+ const noon = new DateTime("2024-01-15T12:00:00.000Z");
49
+ expect((noon as any).format("h")).toBe("12");
50
+ });
51
+
52
+ it("should format minute tokens", () => {
53
+ const dt = new DateTime("2024-01-15T14:05:45.123Z");
54
+ expect((dt as any).format("mm")).toBe("05");
55
+ expect((dt as any).format("m")).toBe("5");
56
+ });
57
+
58
+ it("should format second tokens", () => {
59
+ const dt = new DateTime("2024-01-15T14:30:05.123Z");
60
+ expect((dt as any).format("ss")).toBe("05");
61
+ expect((dt as any).format("s")).toBe("5");
62
+ });
63
+
64
+ it("should format millisecond tokens", () => {
65
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
66
+ expect((dt as any).format("SSS")).toBe("123");
67
+
68
+ const dtZero = new DateTime("2024-01-15T14:30:45.000Z");
69
+ expect((dtZero as any).format("SSS")).toBe("000");
70
+ });
71
+
72
+ it("should format AM/PM tokens", () => {
73
+ const dtAM = new DateTime("2024-01-15T09:30:45.123Z");
74
+ expect((dtAM as any).format("A")).toBe("AM");
75
+ expect((dtAM as any).format("a")).toBe("am");
76
+
77
+ const dtPM = new DateTime("2024-01-15T14:30:45.123Z");
78
+ expect((dtPM as any).format("A")).toBe("PM");
79
+ expect((dtPM as any).format("a")).toBe("pm");
80
+ });
81
+
82
+ it("should format day of week tokens", () => {
83
+ // 2024-01-15 is a Monday
84
+ const monday = new DateTime("2024-01-15T12:00:00.000Z");
85
+ expect((monday as any).format("dddd")).toBe("Monday");
86
+ expect((monday as any).format("ddd")).toBe("Mon");
87
+ expect((monday as any).format("dd")).toBe("Mo");
88
+ expect((monday as any).format("d")).toBe("1");
89
+
90
+ // Sunday should be 0
91
+ const sunday = new DateTime("2024-01-21T12:00:00.000Z");
92
+ expect((sunday as any).format("d")).toBe("0");
93
+ });
94
+
95
+ it("should format month name tokens", () => {
96
+ const january = new DateTime("2024-01-15T12:00:00.000Z");
97
+ expect((january as any).format("MMMM")).toBe("January");
98
+ expect((january as any).format("MMM")).toBe("Jan");
99
+
100
+ const december = new DateTime("2024-12-15T12:00:00.000Z");
101
+ expect((december as any).format("MMMM")).toBe("December");
102
+ expect((december as any).format("MMM")).toBe("Dec");
103
+ });
104
+
105
+ it("should format complex format strings", () => {
106
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
107
+ expect((dt as any).format("YYYY-MM-DD")).toBe("2024-01-15");
108
+ expect((dt as any).format("MM/DD/YYYY")).toBe("01/15/2024");
109
+ expect((dt as any).format("YYYY-MM-DD HH:mm:ss")).toBe(
110
+ "2024-01-15 14:30:45",
111
+ );
112
+ expect((dt as any).format("YYYY-MM-DD HH:mm:ss.SSS")).toBe(
113
+ "2024-01-15 14:30:45.123",
114
+ );
115
+ });
116
+
117
+ it("should format with text separators", () => {
118
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
119
+ expect((dt as any).format("MMMM D, YYYY")).toBe("January 15, 2024");
120
+ expect((dt as any).format("dddd, MMMM D, YYYY")).toBe(
121
+ "Monday, January 15, 2024",
122
+ );
123
+ expect((dt as any).format("h:mm A")).toBe("2:30 PM");
124
+ expect((dt as any).format("hh:mm:ss a")).toBe("02:30:45 pm");
125
+ });
126
+
127
+ it("should handle escaped characters in brackets", () => {
128
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
129
+ expect((dt as any).format("[Year:] YYYY")).toBe("Year: 2024");
130
+ expect((dt as any).format("YYYY [escaped] MM")).toBe(
131
+ "2024 escaped 01",
132
+ );
133
+ expect((dt as any).format("[YYYY] YYYY")).toBe("YYYY 2024");
134
+ });
135
+
136
+ it("should return default format when no format string provided", () => {
137
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
138
+ const result = (dt as any).format();
139
+ // Should return ISO string or a default format
140
+ expect(result).toBeDefined();
141
+ expect(typeof result).toBe("string");
142
+ });
143
+
144
+ it("should handle edge cases", () => {
145
+ // Midnight
146
+ const midnight = new DateTime("2024-01-15T00:00:00.000Z");
147
+ expect((midnight as any).format("HH:mm:ss")).toBe("00:00:00");
148
+ expect((midnight as any).format("hh:mm:ss A")).toBe("12:00:00 AM");
149
+
150
+ // Single digit values
151
+ const dt = new DateTime("2024-01-05T09:05:05.005Z");
152
+ expect((dt as any).format("M/D/YY h:m:s")).toBe("1/5/24 9:5:5");
153
+ });
154
+
155
+ it("should preserve literal text", () => {
156
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
157
+ expect((dt as any).format("[Today is] YYYY-MM-DD")).toBe(
158
+ "Today is 2024-01-15",
159
+ );
160
+ expect((dt as any).format("[The time is] HH:mm")).toBe(
161
+ "The time is 14:30",
162
+ );
163
+ });
164
+ });
165
+
166
+ describe("Chaining", () => {
167
+ it("should work with other DateTime methods", () => {
168
+ const dt = new DateTime("2024-01-15T14:30:45.123Z");
169
+ const formatted = (dt.addDays(1) as any).format("YYYY-MM-DD");
170
+ expect(formatted).toBe("2024-01-16");
171
+ });
172
+ });
173
+ });
@@ -0,0 +1,78 @@
1
+ import type { DateTime } from "../../core/datetime";
2
+ import type { DateTimeClass } from "../../types";
3
+ import { createPlugin } from "../plugin-base";
4
+ import { parseFormat } from "./tokens";
5
+
6
+ /**
7
+ * Default format string
8
+ */
9
+ const DEFAULT_FORMAT = "YYYY-MM-DDTHH:mm:ss.SSSZ";
10
+
11
+ /**
12
+ * Extended DateTime interface with format method
13
+ */
14
+ declare module "../../core/datetime" {
15
+ interface DateTime {
16
+ /**
17
+ * Formats the date using a format string
18
+ * @param formatStr - Format string with tokens (default: ISO format)
19
+ * @returns Formatted date string
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * dt.format("YYYY-MM-DD") // "2024-01-15"
24
+ * dt.format("MM/DD/YYYY") // "01/15/2024"
25
+ * dt.format("MMMM D, YYYY") // "January 15, 2024"
26
+ * dt.format("h:mm A") // "2:30 PM"
27
+ * dt.format("[Year:] YYYY") // "Year: 2024"
28
+ * ```
29
+ *
30
+ * Available tokens:
31
+ * - YYYY: 4-digit year
32
+ * - YY: 2-digit year
33
+ * - MMMM: Full month name
34
+ * - MMM: Abbreviated month name
35
+ * - MM: 2-digit month
36
+ * - M: Month number
37
+ * - DD: 2-digit day of month
38
+ * - D: Day of month
39
+ * - dddd: Full day name
40
+ * - ddd: Abbreviated day name
41
+ * - dd: Min day name
42
+ * - d: Day of week (0-6)
43
+ * - HH: 2-digit hour (24-hour)
44
+ * - H: Hour (24-hour)
45
+ * - hh: 2-digit hour (12-hour)
46
+ * - h: Hour (12-hour)
47
+ * - mm: 2-digit minute
48
+ * - m: Minute
49
+ * - ss: 2-digit second
50
+ * - s: Second
51
+ * - SSS: Millisecond
52
+ * - A: AM/PM (uppercase)
53
+ * - a: am/pm (lowercase)
54
+ * - [text]: Escaped text (literal)
55
+ */
56
+ format(formatStr?: string): string;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Format plugin for DateTime
62
+ * Adds format() method for custom date formatting
63
+ */
64
+ export const formatPlugin = createPlugin(
65
+ "format",
66
+ (DateTimeClass: DateTimeClass) => {
67
+ // Add instance method
68
+ DateTimeClass.prototype.format = function (formatStr?: string): string {
69
+ // If no format string provided, return ISO string
70
+ if (!formatStr) {
71
+ return this.toISO();
72
+ }
73
+
74
+ // Parse the format string and replace tokens
75
+ return parseFormat(this, formatStr);
76
+ };
77
+ },
78
+ );
@@ -0,0 +1,153 @@
1
+ import type { DateTime } from "../../core/datetime";
2
+
3
+ /**
4
+ * Pads a number with leading zeros
5
+ */
6
+ function pad(num: number, length: number): string {
7
+ return num.toString().padStart(length, "0");
8
+ }
9
+
10
+ /**
11
+ * Month names (full)
12
+ */
13
+ const MONTH_NAMES = [
14
+ "January",
15
+ "February",
16
+ "March",
17
+ "April",
18
+ "May",
19
+ "June",
20
+ "July",
21
+ "August",
22
+ "September",
23
+ "October",
24
+ "November",
25
+ "December",
26
+ ];
27
+
28
+ /**
29
+ * Month names (abbreviated)
30
+ */
31
+ const MONTH_NAMES_SHORT = [
32
+ "Jan",
33
+ "Feb",
34
+ "Mar",
35
+ "Apr",
36
+ "May",
37
+ "Jun",
38
+ "Jul",
39
+ "Aug",
40
+ "Sep",
41
+ "Oct",
42
+ "Nov",
43
+ "Dec",
44
+ ];
45
+
46
+ /**
47
+ * Day of week names (full)
48
+ */
49
+ const DAY_NAMES = [
50
+ "Sunday",
51
+ "Monday",
52
+ "Tuesday",
53
+ "Wednesday",
54
+ "Thursday",
55
+ "Friday",
56
+ "Saturday",
57
+ ];
58
+
59
+ /**
60
+ * Day of week names (abbreviated)
61
+ */
62
+ const DAY_NAMES_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
63
+
64
+ /**
65
+ * Day of week names (two letters)
66
+ */
67
+ const DAY_NAMES_MIN = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
68
+
69
+ /**
70
+ * Format token definitions
71
+ * Maps token strings to functions that extract the value from a DateTime instance
72
+ */
73
+ export const FORMAT_TOKENS: Record<string, (dt: DateTime) => string> = {
74
+ // Year
75
+ YYYY: (dt) => dt.year().toString(),
76
+ YY: (dt) => pad(dt.year() % 100, 2),
77
+
78
+ // Month
79
+ MMMM: (dt) => MONTH_NAMES[dt.month()]!,
80
+ MMM: (dt) => MONTH_NAMES_SHORT[dt.month()]!,
81
+ MM: (dt) => pad(dt.month() + 1, 2),
82
+ M: (dt) => (dt.month() + 1).toString(),
83
+
84
+ // Day of Month
85
+ DD: (dt) => pad(dt.date(), 2),
86
+ D: (dt) => dt.date().toString(),
87
+
88
+ // Day of Week
89
+ dddd: (dt) => DAY_NAMES[dt.day()]!,
90
+ ddd: (dt) => DAY_NAMES_SHORT[dt.day()]!,
91
+ dd: (dt) => DAY_NAMES_MIN[dt.day()]!,
92
+
93
+ // Hour (24-hour)
94
+ HH: (dt) => pad(dt.hour(), 2),
95
+ H: (dt) => dt.hour().toString(),
96
+
97
+ // Hour (12-hour)
98
+ hh: (dt) => {
99
+ const hour = dt.hour();
100
+ const hour12 = hour % 12 || 12;
101
+ return pad(hour12, 2);
102
+ },
103
+ h: (dt) => {
104
+ const hour = dt.hour();
105
+ const hour12 = hour % 12 || 12;
106
+ return hour12.toString();
107
+ },
108
+
109
+ // Minute
110
+ mm: (dt) => pad(dt.minute(), 2),
111
+ m: (dt) => dt.minute().toString(),
112
+
113
+ // Second
114
+ ss: (dt) => pad(dt.second(), 2),
115
+ s: (dt) => dt.second().toString(),
116
+
117
+ // Millisecond
118
+ SSS: (dt) => pad(dt.millisecond(), 3),
119
+
120
+ // AM/PM
121
+ A: (dt) => (dt.hour() >= 12 ? "PM" : "AM"),
122
+ a: (dt) => (dt.hour() >= 12 ? "pm" : "am"),
123
+
124
+ // Day of week number
125
+ d: (dt) => dt.day().toString(),
126
+ };
127
+
128
+ /**
129
+ * Regular expression to match format tokens
130
+ * Matches escaped text first, then longest tokens to avoid partial matches
131
+ * Order is critical: longer tokens must come before shorter ones
132
+ */
133
+ export const TOKEN_REGEX =
134
+ /\[([^\]]+)\]|YYYY|MMMM|MMM|MM|M|dddd|ddd|DD|dd|D|d|HH|hh|H|h|mm|m|ss|s|SSS|YY|A|a/g;
135
+
136
+ /**
137
+ * Parses a format string and replaces tokens with actual values
138
+ * @param dt - DateTime instance
139
+ * @param formatStr - Format string with tokens
140
+ * @returns Formatted date string
141
+ */
142
+ export function parseFormat(dt: DateTime, formatStr: string): string {
143
+ return formatStr.replace(TOKEN_REGEX, (match, escapedText) => {
144
+ // If it's escaped text in brackets, return without brackets
145
+ if (escapedText !== undefined) {
146
+ return escapedText;
147
+ }
148
+
149
+ // Otherwise, look up the token and apply it
150
+ const tokenFn = FORMAT_TOKENS[match];
151
+ return tokenFn ? tokenFn(dt) : match;
152
+ });
153
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Plugin utilities for extending DateTime functionality
3
+ *
4
+ * This module provides helpers for creating and validating DateTime plugins.
5
+ * Plugins allow extending the DateTime class with custom methods and functionality.
6
+ *
7
+ * @module plugins
8
+ */
9
+
10
+ export { businessDaysPlugin } from "./business-days/index.ts";
11
+ export { formatPlugin } from "./format/index.ts";
12
+ export { createPlugin, isPlugin, isValidPluginName } from "./plugin-base.ts";
13
+ export { relativeTimePlugin } from "./relative-time/index.ts";
14
+ // Core plugins
15
+ export { timezonePlugin } from "./timezone/index.ts";
@@ -0,0 +1,211 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DateTime } from "../core/datetime.ts";
3
+ import type { DateTimeClass } from "../types.ts";
4
+ import { createPlugin, isPlugin, isValidPluginName } from "./plugin-base.ts";
5
+
6
+ describe("createPlugin", () => {
7
+ test("creates a valid plugin object", () => {
8
+ const plugin = createPlugin("test-plugin", (DateTimeClass) => {
9
+ // Plugin logic
10
+ });
11
+
12
+ expect(plugin).toHaveProperty("name", "test-plugin");
13
+ expect(plugin).toHaveProperty("install");
14
+ expect(typeof plugin.install).toBe("function");
15
+ });
16
+
17
+ test("throws error for empty name", () => {
18
+ expect(() => {
19
+ createPlugin("", () => {});
20
+ }).toThrow("Plugin name must be a non-empty string");
21
+ });
22
+
23
+ test("throws error for non-string name", () => {
24
+ expect(() => {
25
+ createPlugin(123 as any, () => {});
26
+ }).toThrow("Plugin name must be a non-empty string");
27
+
28
+ expect(() => {
29
+ createPlugin(null as any, () => {});
30
+ }).toThrow("Plugin name must be a non-empty string");
31
+
32
+ expect(() => {
33
+ createPlugin(undefined as any, () => {});
34
+ }).toThrow("Plugin name must be a non-empty string");
35
+ });
36
+
37
+ test("throws error for non-function install", () => {
38
+ expect(() => {
39
+ createPlugin("test", "not-a-function" as any);
40
+ }).toThrow("Plugin install must be a function");
41
+
42
+ expect(() => {
43
+ createPlugin("test", null as any);
44
+ }).toThrow("Plugin install must be a function");
45
+
46
+ expect(() => {
47
+ createPlugin("test", undefined as any);
48
+ }).toThrow("Plugin install must be a function");
49
+ });
50
+
51
+ test("creates plugin with various name formats", () => {
52
+ const plugins = [
53
+ createPlugin("camelCase", () => {}),
54
+ createPlugin("kebab-case", () => {}),
55
+ createPlugin("snake_case", () => {}),
56
+ createPlugin("with123numbers", () => {}),
57
+ ];
58
+
59
+ plugins.forEach((plugin, i) => {
60
+ expect(plugin).toHaveProperty("name");
61
+ expect(plugin).toHaveProperty("install");
62
+ expect(typeof plugin.install).toBe("function");
63
+ });
64
+ });
65
+ });
66
+
67
+ describe("isPlugin", () => {
68
+ test("returns true for valid plugin objects", () => {
69
+ const plugin = createPlugin("test", () => {});
70
+ expect(isPlugin(plugin)).toBe(true);
71
+
72
+ const manualPlugin = {
73
+ name: "manual-plugin",
74
+ install: () => {},
75
+ };
76
+ expect(isPlugin(manualPlugin)).toBe(true);
77
+ });
78
+
79
+ test("returns false for invalid objects", () => {
80
+ expect(isPlugin(null)).toBe(false);
81
+ expect(isPlugin(undefined)).toBe(false);
82
+ expect(isPlugin("string")).toBe(false);
83
+ expect(isPlugin(123)).toBe(false);
84
+ expect(isPlugin([])).toBe(false);
85
+ });
86
+
87
+ test("returns false for objects missing required properties", () => {
88
+ expect(isPlugin({ name: "test" })).toBe(false);
89
+ expect(isPlugin({ install: () => {} })).toBe(false);
90
+ expect(isPlugin({})).toBe(false);
91
+ });
92
+
93
+ test("returns false for objects with invalid property types", () => {
94
+ expect(
95
+ isPlugin({
96
+ name: 123,
97
+ install: () => {},
98
+ }),
99
+ ).toBe(false);
100
+
101
+ expect(
102
+ isPlugin({
103
+ name: "test",
104
+ install: "not-a-function",
105
+ }),
106
+ ).toBe(false);
107
+
108
+ expect(
109
+ isPlugin({
110
+ name: "",
111
+ install: () => {},
112
+ }),
113
+ ).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("isValidPluginName", () => {
118
+ test("returns true for valid plugin names", () => {
119
+ expect(isValidPluginName("test")).toBe(true);
120
+ expect(isValidPluginName("test-plugin")).toBe(true);
121
+ expect(isValidPluginName("testPlugin")).toBe(true);
122
+ expect(isValidPluginName("test_plugin")).toBe(true);
123
+ expect(isValidPluginName("test123")).toBe(true);
124
+ expect(isValidPluginName("TEST")).toBe(true);
125
+ expect(isValidPluginName("my-awesome-plugin")).toBe(true);
126
+ });
127
+
128
+ test("returns false for invalid plugin names", () => {
129
+ expect(isValidPluginName("")).toBe(false);
130
+ expect(isValidPluginName("a")).toBe(false); // Too short
131
+ expect(isValidPluginName("test plugin")).toBe(false); // Space
132
+ expect(isValidPluginName("test@plugin")).toBe(false); // Special char
133
+ expect(isValidPluginName("test.plugin")).toBe(false); // Period
134
+ expect(isValidPluginName("test/plugin")).toBe(false); // Slash
135
+ expect(isValidPluginName("-test")).toBe(false); // Starts with hyphen
136
+ expect(isValidPluginName("test-")).toBe(false); // Ends with hyphen
137
+ expect(isValidPluginName("_test")).toBe(false); // Starts with underscore
138
+ expect(isValidPluginName("test_")).toBe(false); // Ends with underscore
139
+ });
140
+
141
+ test("returns false for non-string values", () => {
142
+ expect(isValidPluginName(null as any)).toBe(false);
143
+ expect(isValidPluginName(undefined as any)).toBe(false);
144
+ expect(isValidPluginName(123 as any)).toBe(false);
145
+ expect(isValidPluginName({} as any)).toBe(false);
146
+ expect(isValidPluginName([] as any)).toBe(false);
147
+ });
148
+ });
149
+
150
+ describe("plugin integration", () => {
151
+ test("createPlugin works with DateTime.extend", () => {
152
+ const plugin = createPlugin("test-integration", (DateTimeClass) => {
153
+ // Add a test method to the prototype
154
+ (DateTimeClass.prototype as any).testMethod = () => "test-result";
155
+ });
156
+
157
+ // Register the plugin
158
+ DateTime.extend(plugin);
159
+
160
+ // Check that plugin is registered
161
+ expect(DateTime.hasPlugin("test-integration")).toBe(true);
162
+
163
+ // Check that method was added
164
+ const dt = new DateTime();
165
+ expect((dt as any).testMethod()).toBe("test-result");
166
+ });
167
+
168
+ test("plugin install function receives options", () => {
169
+ let receivedOptions: Record<string, unknown> | undefined;
170
+
171
+ const plugin = createPlugin("test-options", (DateTimeClass, options) => {
172
+ receivedOptions = options;
173
+ });
174
+
175
+ const options = { foo: "bar", baz: 123 };
176
+ DateTime.extend(plugin, options);
177
+
178
+ expect(receivedOptions).toEqual(options);
179
+ });
180
+
181
+ test("plugin can add static methods", () => {
182
+ const plugin = createPlugin("test-static", (DateTimeClass) => {
183
+ (DateTimeClass as any).staticMethod = () => new DateTimeClass();
184
+ });
185
+
186
+ DateTime.extend(plugin);
187
+
188
+ expect(typeof (DateTime as any).staticMethod).toBe("function");
189
+ expect((DateTime as any).staticMethod()).toBeInstanceOf(DateTime);
190
+ });
191
+
192
+ test("multiple plugins can be registered", () => {
193
+ const plugin1 = createPlugin("multi-plugin-1", (DateTimeClass) => {
194
+ (DateTimeClass.prototype as any).method1 = () => "method1";
195
+ });
196
+
197
+ const plugin2 = createPlugin("multi-plugin-2", (DateTimeClass) => {
198
+ (DateTimeClass.prototype as any).method2 = () => "method2";
199
+ });
200
+
201
+ DateTime.extend(plugin1);
202
+ DateTime.extend(plugin2);
203
+
204
+ expect(DateTime.hasPlugin("multi-plugin-1")).toBe(true);
205
+ expect(DateTime.hasPlugin("multi-plugin-2")).toBe(true);
206
+
207
+ const dt = new DateTime();
208
+ expect((dt as any).method1()).toBe("method1");
209
+ expect((dt as any).method2()).toBe("method2");
210
+ });
211
+ });