@bash-app/bash-common 30.247.0 → 30.254.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.
- package/README.md +2 -0
- package/dist/__tests__/definitionsHelpers.test.d.ts +2 -0
- package/dist/__tests__/definitionsHelpers.test.d.ts.map +1 -0
- package/dist/__tests__/definitionsHelpers.test.js +85 -0
- package/dist/__tests__/definitionsHelpers.test.js.map +1 -0
- package/dist/__tests__/extendedSchemas.test.d.ts +2 -0
- package/dist/__tests__/extendedSchemas.test.d.ts.map +1 -0
- package/dist/__tests__/extendedSchemas.test.js +40 -0
- package/dist/__tests__/extendedSchemas.test.js.map +1 -0
- package/dist/__tests__/membershipDefinitions.test.d.ts +2 -0
- package/dist/__tests__/membershipDefinitions.test.d.ts.map +1 -0
- package/dist/__tests__/membershipDefinitions.test.js +95 -0
- package/dist/__tests__/membershipDefinitions.test.js.map +1 -0
- package/dist/__tests__/ticketAdmission.test.d.ts +5 -0
- package/dist/__tests__/ticketAdmission.test.d.ts.map +1 -0
- package/dist/__tests__/ticketAdmission.test.js +42 -0
- package/dist/__tests__/ticketAdmission.test.js.map +1 -0
- package/dist/__tests__/utmAttribution.test.d.ts +2 -0
- package/dist/__tests__/utmAttribution.test.d.ts.map +1 -0
- package/dist/__tests__/utmAttribution.test.js +92 -0
- package/dist/__tests__/utmAttribution.test.js.map +1 -0
- package/dist/extendedSchemas.d.ts +2 -0
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js +2 -0
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/sms/smsTemplates.d.ts +27 -0
- package/dist/sms/smsTemplates.d.ts.map +1 -0
- package/dist/sms/smsTemplates.js +11 -0
- package/dist/sms/smsTemplates.js.map +1 -0
- package/dist/utils/__tests__/addressUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/addressUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/addressUtils.test.js +138 -0
- package/dist/utils/__tests__/addressUtils.test.js.map +1 -0
- package/dist/utils/__tests__/apiUtils.test.d.ts +5 -0
- package/dist/utils/__tests__/apiUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/apiUtils.test.js +161 -0
- package/dist/utils/__tests__/apiUtils.test.js.map +1 -0
- package/dist/utils/__tests__/arrayUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/arrayUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/arrayUtils.test.js +23 -0
- package/dist/utils/__tests__/arrayUtils.test.js.map +1 -0
- package/dist/utils/__tests__/blogUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/blogUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/blogUtils.test.js +102 -0
- package/dist/utils/__tests__/blogUtils.test.js.map +1 -0
- package/dist/utils/__tests__/dateTimeUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/dateTimeUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/dateTimeUtils.test.js +180 -0
- package/dist/utils/__tests__/dateTimeUtils.test.js.map +1 -0
- package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts +2 -0
- package/dist/utils/__tests__/discountEngine/discountCalculator.test.d.ts.map +1 -0
- package/dist/utils/__tests__/discountEngine/discountCalculator.test.js +356 -0
- package/dist/utils/__tests__/discountEngine/discountCalculator.test.js.map +1 -0
- package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/generalDateTimeUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/generalDateTimeUtils.test.js +26 -0
- package/dist/utils/__tests__/generalDateTimeUtils.test.js.map +1 -0
- package/dist/utils/__tests__/luxonUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/luxonUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/luxonUtils.test.js +139 -0
- package/dist/utils/__tests__/luxonUtils.test.js.map +1 -0
- package/dist/utils/__tests__/mathUtils.test.d.ts +5 -0
- package/dist/utils/__tests__/mathUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/mathUtils.test.js +26 -0
- package/dist/utils/__tests__/mathUtils.test.js.map +1 -0
- package/dist/utils/__tests__/objUtils.test.d.ts +5 -0
- package/dist/utils/__tests__/objUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/objUtils.test.js +41 -0
- package/dist/utils/__tests__/objUtils.test.js.map +1 -0
- package/dist/utils/__tests__/paymentUtils.test.js +48 -1
- package/dist/utils/__tests__/paymentUtils.test.js.map +1 -1
- package/dist/utils/__tests__/qrCodeUtils.test.js +16 -1
- package/dist/utils/__tests__/qrCodeUtils.test.js.map +1 -1
- package/dist/utils/__tests__/reviewUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/reviewUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/reviewUtils.test.js +49 -0
- package/dist/utils/__tests__/reviewUtils.test.js.map +1 -0
- package/dist/utils/__tests__/slugUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/slugUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/slugUtils.test.js +103 -0
- package/dist/utils/__tests__/slugUtils.test.js.map +1 -0
- package/dist/utils/__tests__/sortUtils.test.d.ts +5 -0
- package/dist/utils/__tests__/sortUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/sortUtils.test.js +28 -0
- package/dist/utils/__tests__/sortUtils.test.js.map +1 -0
- package/dist/utils/__tests__/stringUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/stringUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/stringUtils.test.js +27 -0
- package/dist/utils/__tests__/stringUtils.test.js.map +1 -0
- package/dist/utils/__tests__/ticketListUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/ticketListUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/ticketListUtils.test.js +41 -0
- package/dist/utils/__tests__/ticketListUtils.test.js.map +1 -0
- package/dist/utils/__tests__/typeUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/typeUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/typeUtils.test.js +11 -0
- package/dist/utils/__tests__/typeUtils.test.js.map +1 -0
- package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts +5 -0
- package/dist/utils/__tests__/userSubscriptionUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/userSubscriptionUtils.test.js +66 -0
- package/dist/utils/__tests__/userSubscriptionUtils.test.js.map +1 -0
- package/dist/utils/__tests__/userUtils.test.d.ts +2 -0
- package/dist/utils/__tests__/userUtils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/userUtils.test.js +34 -0
- package/dist/utils/__tests__/userUtils.test.js.map +1 -0
- package/dist/utils/__tests__/venueTypeFormatter.test.d.ts +2 -0
- package/dist/utils/__tests__/venueTypeFormatter.test.d.ts.map +1 -0
- package/dist/utils/__tests__/venueTypeFormatter.test.js +25 -0
- package/dist/utils/__tests__/venueTypeFormatter.test.js.map +1 -0
- package/dist/utils/service/__tests__/regexUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/regexUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/regexUtils.test.js +51 -0
- package/dist/utils/service/__tests__/regexUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/serviceDBUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/serviceDBUtils.test.js +17 -0
- package/dist/utils/service/__tests__/serviceDBUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js +60 -0
- package/dist/utils/service/__tests__/serviceFeaturedSortUtils.test.js.map +1 -0
- package/dist/utils/service/__tests__/serviceUtils.test.d.ts +2 -0
- package/dist/utils/service/__tests__/serviceUtils.test.d.ts.map +1 -0
- package/dist/utils/service/__tests__/serviceUtils.test.js +135 -0
- package/dist/utils/service/__tests__/serviceUtils.test.js.map +1 -0
- package/dist/utils/service/serviceFeaturedSortUtils.d.ts.map +1 -1
- package/dist/utils/service/serviceFeaturedSortUtils.js +5 -1
- package/dist/utils/service/serviceFeaturedSortUtils.js.map +1 -1
- package/dist/utils/ticketListUtils.d.ts.map +1 -1
- package/dist/utils/ticketListUtils.js +11 -7
- package/dist/utils/ticketListUtils.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +47 -0
- package/src/__tests__/definitionsHelpers.test.ts +136 -0
- package/src/__tests__/extendedSchemas.test.ts +50 -0
- package/src/__tests__/membershipDefinitions.test.ts +144 -0
- package/src/__tests__/ticketAdmission.test.ts +73 -0
- package/src/__tests__/utmAttribution.test.ts +118 -0
- package/src/extendedSchemas.ts +2 -0
- package/src/index.ts +1 -0
- package/src/sms/smsTemplates.ts +20 -0
- package/src/utils/__tests__/addressUtils.test.ts +176 -0
- package/src/utils/__tests__/apiUtils.test.ts +219 -0
- package/src/utils/__tests__/arrayUtils.test.ts +26 -0
- package/src/utils/__tests__/blogUtils.test.ts +145 -0
- package/src/utils/__tests__/dateTimeUtils.test.ts +233 -0
- package/src/utils/__tests__/discountEngine/discountCalculator.test.ts +410 -0
- package/src/utils/__tests__/generalDateTimeUtils.test.ts +34 -0
- package/src/utils/__tests__/luxonUtils.test.ts +171 -0
- package/src/utils/__tests__/mathUtils.test.ts +30 -0
- package/src/utils/__tests__/objUtils.test.ts +48 -0
- package/src/utils/__tests__/paymentUtils.test.ts +59 -0
- package/src/utils/__tests__/qrCodeUtils.test.ts +28 -1
- package/src/utils/__tests__/reviewUtils.test.ts +65 -0
- package/src/utils/__tests__/slugUtils.test.ts +143 -0
- package/src/utils/__tests__/sortUtils.test.ts +33 -0
- package/src/utils/__tests__/stringUtils.test.ts +37 -0
- package/src/utils/__tests__/ticketListUtils.test.ts +47 -0
- package/src/utils/__tests__/typeUtils.test.ts +12 -0
- package/src/utils/__tests__/userSubscriptionUtils.test.ts +87 -0
- package/src/utils/__tests__/userUtils.test.ts +41 -0
- package/src/utils/__tests__/venueTypeFormatter.test.ts +31 -0
- package/src/utils/service/__tests__/regexUtils.test.ts +66 -0
- package/src/utils/service/__tests__/serviceDBUtils.test.ts +19 -0
- package/src/utils/service/__tests__/serviceFeaturedSortUtils.test.ts +67 -0
- package/src/utils/service/__tests__/serviceUtils.test.ts +190 -0
- package/src/utils/service/serviceFeaturedSortUtils.ts +5 -1
- package/src/utils/ticketListUtils.ts +13 -6
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { DayOfWeek } from "@prisma/client";
|
|
2
|
+
import { DateTime } from "luxon";
|
|
3
|
+
import { describe, expect, test } from "@jest/globals";
|
|
4
|
+
import {
|
|
5
|
+
dateRangeDurationMatch,
|
|
6
|
+
dateTimeDiffHours,
|
|
7
|
+
dateTimeFormat,
|
|
8
|
+
dateTimeFormatDateRange,
|
|
9
|
+
dateTimeFromDate,
|
|
10
|
+
dateTimeFromDayTime,
|
|
11
|
+
dateTimeRangeCrossesMidnight,
|
|
12
|
+
dateTimeToTimeComponents,
|
|
13
|
+
dateTimeWithinRange,
|
|
14
|
+
dayOfWeekFromIdx,
|
|
15
|
+
dayOfWeekGetIdx,
|
|
16
|
+
filterConflictingIntervals,
|
|
17
|
+
luxonDateRangeSort,
|
|
18
|
+
mergeCrossDayHours,
|
|
19
|
+
utcOffsetMinutesToTimezone,
|
|
20
|
+
} from "../luxonUtils.js";
|
|
21
|
+
|
|
22
|
+
describe("luxonUtils", () => {
|
|
23
|
+
describe("day of week helpers", () => {
|
|
24
|
+
test("dayOfWeekGetIdx accepts enum and numeric index", () => {
|
|
25
|
+
expect(dayOfWeekGetIdx(DayOfWeek.Wednesday)).toBe(3);
|
|
26
|
+
expect(dayOfWeekGetIdx(5)).toBe(5);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("dayOfWeekFromIdx accepts index and enum passthrough", () => {
|
|
30
|
+
expect(dayOfWeekFromIdx(4)).toBe(DayOfWeek.Thursday);
|
|
31
|
+
expect(dayOfWeekFromIdx(DayOfWeek.Friday)).toBe(DayOfWeek.Friday);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("dateTimeFromDayTime builds weekday time in zone", () => {
|
|
35
|
+
const dt = dateTimeFromDayTime(DayOfWeek.Monday, 14, 30, "UTC");
|
|
36
|
+
expect(dt.weekday).toBe(1);
|
|
37
|
+
expect(dt.hour).toBe(14);
|
|
38
|
+
expect(dt.minute).toBe(30);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("dateTimeFromDate and formatting", () => {
|
|
43
|
+
test("dateTimeFromDate parses ISO string", () => {
|
|
44
|
+
const dt = dateTimeFromDate("2024-03-15T18:00:00.000Z", "UTC");
|
|
45
|
+
expect(dt.year).toBe(2024);
|
|
46
|
+
expect(dt.month).toBe(3);
|
|
47
|
+
expect(dt.day).toBe(15);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("dateTimeToTimeComponents extracts hour and minute", () => {
|
|
51
|
+
const dt = DateTime.fromObject({ hour: 9, minute: 15 }, { zone: "UTC" });
|
|
52
|
+
expect(dateTimeToTimeComponents(dt)).toEqual({ hour: 9, minute: 15 });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("dateTimeFormat returns empty string for nullish", () => {
|
|
56
|
+
expect(dateTimeFormat(null)).toBe("");
|
|
57
|
+
expect(dateTimeFormat(undefined)).toBe("");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("range helpers", () => {
|
|
62
|
+
const start = DateTime.fromISO("2024-01-01T10:00:00", { zone: "UTC" });
|
|
63
|
+
const end = DateTime.fromISO("2024-01-01T18:00:00", { zone: "UTC" });
|
|
64
|
+
const range = { start, end };
|
|
65
|
+
|
|
66
|
+
test("dateTimeWithinRange detects containment", () => {
|
|
67
|
+
const inside = DateTime.fromISO("2024-01-01T12:00:00", { zone: "UTC" });
|
|
68
|
+
const outside = DateTime.fromISO("2024-01-01T19:00:00", { zone: "UTC" });
|
|
69
|
+
expect(dateTimeWithinRange(inside, range)).toBe(true);
|
|
70
|
+
expect(dateTimeWithinRange(outside, range)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("dateRangeDurationMatch compares durations within tolerance", () => {
|
|
74
|
+
const lhs = {
|
|
75
|
+
start: DateTime.fromISO("2024-01-01T10:00:00", { zone: "UTC" }),
|
|
76
|
+
end: DateTime.fromISO("2024-01-01T12:00:00", { zone: "UTC" }),
|
|
77
|
+
};
|
|
78
|
+
const rhs = {
|
|
79
|
+
start: DateTime.fromISO("2024-02-01T10:00:00", { zone: "UTC" }),
|
|
80
|
+
end: DateTime.fromISO("2024-02-01T12:00:30", { zone: "UTC" }),
|
|
81
|
+
};
|
|
82
|
+
expect(dateRangeDurationMatch(lhs, rhs, 60)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("dateTimeFormatDateRange uses time-only end on same day", () => {
|
|
86
|
+
const sameDayStart = DateTime.fromISO("2024-06-01T18:00:00", { zone: "UTC" });
|
|
87
|
+
const sameDayEnd = DateTime.fromISO("2024-06-01T22:00:00", { zone: "UTC" });
|
|
88
|
+
const formatted = dateTimeFormatDateRange(sameDayStart, sameDayEnd);
|
|
89
|
+
expect(formatted).toContain(" to ");
|
|
90
|
+
expect(formatted.split(" to ")[1]).toMatch(/\d/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("dateTimeRangeCrossesMidnight detects overnight ranges", () => {
|
|
94
|
+
const overnight = {
|
|
95
|
+
start: DateTime.fromObject({ hour: 22, minute: 0 }, { zone: "UTC" }),
|
|
96
|
+
end: DateTime.fromObject({ hour: 2, minute: 0 }, { zone: "UTC" }),
|
|
97
|
+
};
|
|
98
|
+
const sameDay = {
|
|
99
|
+
start: DateTime.fromObject({ hour: 9, minute: 0 }, { zone: "UTC" }),
|
|
100
|
+
end: DateTime.fromObject({ hour: 17, minute: 0 }, { zone: "UTC" }),
|
|
101
|
+
};
|
|
102
|
+
expect(dateTimeRangeCrossesMidnight(overnight)).toBe(true);
|
|
103
|
+
expect(dateTimeRangeCrossesMidnight(sameDay)).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("mergeCrossDayHours", () => {
|
|
108
|
+
test("merges ranges separated by one minute or less", () => {
|
|
109
|
+
const base = DateTime.fromObject({ hour: 9 }, { zone: "UTC" });
|
|
110
|
+
const merged = mergeCrossDayHours([
|
|
111
|
+
{ start: base, end: base.plus({ hours: 2 }) },
|
|
112
|
+
{ start: base.plus({ hours: 2, minutes: 1 }), end: base.plus({ hours: 4 }) },
|
|
113
|
+
]);
|
|
114
|
+
expect(merged).toHaveLength(1);
|
|
115
|
+
expect(merged[0].end.hour).toBe(13);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("keeps separate ranges when gap exceeds one minute", () => {
|
|
119
|
+
const base = DateTime.fromObject({ hour: 9 }, { zone: "UTC" });
|
|
120
|
+
const merged = mergeCrossDayHours([
|
|
121
|
+
{ start: base, end: base.plus({ hours: 1 }) },
|
|
122
|
+
{ start: base.plus({ hours: 2 }), end: base.plus({ hours: 3 }) },
|
|
123
|
+
]);
|
|
124
|
+
expect(merged).toHaveLength(2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("filterConflictingIntervals", () => {
|
|
129
|
+
test("subtracts blocked hours from available hours", () => {
|
|
130
|
+
const day = DateTime.fromObject({ hour: 8 }, { zone: "UTC" });
|
|
131
|
+
const available = [{ start: day, end: day.plus({ hours: 8 }) }];
|
|
132
|
+
const blocked = [
|
|
133
|
+
{
|
|
134
|
+
start: day.plus({ hours: 2 }),
|
|
135
|
+
end: day.plus({ hours: 3 }),
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
const remaining = filterConflictingIntervals(available, blocked);
|
|
139
|
+
expect(remaining.length).toBeGreaterThan(0);
|
|
140
|
+
expect(remaining.every((r) => r.end > r.start)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("utcOffsetMinutesToTimezone", () => {
|
|
145
|
+
test("formats positive and negative offsets", () => {
|
|
146
|
+
expect(utcOffsetMinutesToTimezone(300)).toBe("UTC+05:00");
|
|
147
|
+
expect(utcOffsetMinutesToTimezone(-480)).toBe("UTC-08:00");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("dateTimeDiffHours", () => {
|
|
152
|
+
test("returns 0 when start or end missing", () => {
|
|
153
|
+
expect(dateTimeDiffHours(null, DateTime.now())).toBe(0);
|
|
154
|
+
expect(dateTimeDiffHours(DateTime.now(), null)).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("luxonDateRangeSort", () => {
|
|
159
|
+
test("sorts ranges by start time ascending", () => {
|
|
160
|
+
const later = {
|
|
161
|
+
start: DateTime.fromISO("2024-01-02T10:00:00", { zone: "UTC" }),
|
|
162
|
+
end: DateTime.fromISO("2024-01-02T12:00:00", { zone: "UTC" }),
|
|
163
|
+
};
|
|
164
|
+
const earlier = {
|
|
165
|
+
start: DateTime.fromISO("2024-01-01T10:00:00", { zone: "UTC" }),
|
|
166
|
+
end: DateTime.fromISO("2024-01-01T12:00:00", { zone: "UTC" }),
|
|
167
|
+
};
|
|
168
|
+
expect(luxonDateRangeSort([later, earlier])).toEqual([earlier, later]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for numeric rounding helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import { roundToNearestDivisor } from "../mathUtils.js";
|
|
7
|
+
|
|
8
|
+
describe("mathUtils", () => {
|
|
9
|
+
describe("roundToNearestDivisor", () => {
|
|
10
|
+
test("rounds to nearest cent (divisor 100)", () => {
|
|
11
|
+
expect(roundToNearestDivisor(12.345, 100)).toBe(12.35);
|
|
12
|
+
expect(roundToNearestDivisor(12.344, 100)).toBe(12.34);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("rounds to nearest tenth (divisor 10)", () => {
|
|
16
|
+
expect(roundToNearestDivisor(3.14, 10)).toBe(3.1);
|
|
17
|
+
expect(roundToNearestDivisor(3.16, 10)).toBe(3.2);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns whole numbers unchanged when divisor is 1", () => {
|
|
21
|
+
expect(roundToNearestDivisor(42, 1)).toBe(42);
|
|
22
|
+
expect(roundToNearestDivisor(42.6, 1)).toBe(43);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("handles zero and negative values", () => {
|
|
26
|
+
expect(roundToNearestDivisor(0, 100)).toBe(0);
|
|
27
|
+
expect(roundToNearestDivisor(-1.235, 100)).toBe(-1.24);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for object type guards and deep merge.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import { isObject, deepMerge } from "../objUtils.js";
|
|
7
|
+
|
|
8
|
+
describe("objUtils", () => {
|
|
9
|
+
describe("isObject", () => {
|
|
10
|
+
test("returns true for plain objects", () => {
|
|
11
|
+
expect(isObject({ a: 1 })).toBe(true);
|
|
12
|
+
expect(isObject(Object.create(null))).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("returns false for null, arrays, dates, and primitives", () => {
|
|
16
|
+
expect(isObject(null)).toBe(false);
|
|
17
|
+
expect(isObject([1, 2])).toBe(false);
|
|
18
|
+
expect(isObject(new Date())).toBe(false);
|
|
19
|
+
expect(isObject("string")).toBe(false);
|
|
20
|
+
expect(isObject(42)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("deepMerge", () => {
|
|
25
|
+
test("overwrites scalar values from source", () => {
|
|
26
|
+
expect(deepMerge({ a: 1, b: 2 }, { b: 99 })).toEqual({ a: 1, b: 99 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("recursively merges nested objects", () => {
|
|
30
|
+
expect(deepMerge({ nested: { x: 1, y: 2 } }, { nested: { y: 3, z: 4 } })).toEqual({
|
|
31
|
+
nested: { x: 1, y: 3, z: 4 },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("concatenates arrays when mergeArrays is true", () => {
|
|
36
|
+
expect(deepMerge({ tags: ["a"] }, { tags: ["b"] }, true)).toEqual({ tags: ["a", "b"] });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("replaces arrays by default", () => {
|
|
40
|
+
expect(deepMerge({ tags: ["a"] }, { tags: ["b"] })).toEqual({ tags: ["b"] });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("handles null/undefined target or source", () => {
|
|
44
|
+
expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
|
|
45
|
+
expect(deepMerge({ a: 1 }, null)).toEqual({ a: 1 });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
calculateTotalBashPoints,
|
|
17
17
|
calculateStripeDonationFee,
|
|
18
18
|
calculateServiceFeeWithMembershipDiscount,
|
|
19
|
+
calculatePartnerFeeWithMembershipDiscount,
|
|
19
20
|
calculateBashPlatformFeeCents,
|
|
20
21
|
splitBashPlatformFeeForTicketCheckout,
|
|
21
22
|
computeTicketApplicationFeeAmountCents,
|
|
@@ -24,7 +25,9 @@ import {
|
|
|
24
25
|
getBashPassMonthlyCapRemaining,
|
|
25
26
|
higherBashPassTier,
|
|
26
27
|
membershipGrantsBashPassTier,
|
|
28
|
+
VENUE_PRICING_PLAN_DATA,
|
|
27
29
|
} from "../paymentUtils.js";
|
|
30
|
+
import { VenuePricingPlan } from "@prisma/client";
|
|
28
31
|
|
|
29
32
|
describe("PaymentUtils - Core Functions", () => {
|
|
30
33
|
describe("Currency Conversion", () => {
|
|
@@ -337,6 +340,62 @@ describe("PaymentUtils - Core Functions", () => {
|
|
|
337
340
|
});
|
|
338
341
|
});
|
|
339
342
|
|
|
343
|
+
describe("Partner fee with membership discount", () => {
|
|
344
|
+
test("calculatePartnerFeeWithMembershipDiscount scales with amount", () => {
|
|
345
|
+
const small = calculatePartnerFeeWithMembershipDiscount(1000, MembershipTier.Basic);
|
|
346
|
+
const large = calculatePartnerFeeWithMembershipDiscount(100000, MembershipTier.Basic);
|
|
347
|
+
expect(large).toBeGreaterThan(small);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("calculatePartnerFeeWithMembershipDiscount defaults to Basic tier", () => {
|
|
351
|
+
const withTier = calculatePartnerFeeWithMembershipDiscount(5000, MembershipTier.Pro);
|
|
352
|
+
const defaulted = calculatePartnerFeeWithMembershipDiscount(5000, null);
|
|
353
|
+
expect(typeof withTier).toBe("number");
|
|
354
|
+
expect(typeof defaulted).toBe("number");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("VENUE_PRICING_PLAN_DATA", () => {
|
|
359
|
+
test("Percentage plan uses pay-per-booking copy and fee", () => {
|
|
360
|
+
const plan = VENUE_PRICING_PLAN_DATA[VenuePricingPlan.Percentage];
|
|
361
|
+
expect(plan.name).toBe("Pay per booking");
|
|
362
|
+
expect(plan.percentageFee).toBeGreaterThan(0);
|
|
363
|
+
expect(plan.monthlyFee).toBe(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("Subscription plan is flat monthly with no percentage fee", () => {
|
|
367
|
+
const plan = VENUE_PRICING_PLAN_DATA[VenuePricingPlan.Subscription];
|
|
368
|
+
expect(plan.monthlyFee).toBe(100);
|
|
369
|
+
expect(plan.percentageFee).toBe(0);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe("splitBashPlatformFeeForTicketCheckout edge cases", () => {
|
|
374
|
+
test("returns zeros when platform fee disabled", () => {
|
|
375
|
+
expect(
|
|
376
|
+
splitBashPlatformFeeForTicketCheckout({
|
|
377
|
+
grossCents: 10000,
|
|
378
|
+
platformFeeEnabled: false,
|
|
379
|
+
platformFeeRate: 0.03,
|
|
380
|
+
feeHandling: "GuestPays",
|
|
381
|
+
bashPassWaivesGuestFee: false,
|
|
382
|
+
})
|
|
383
|
+
).toEqual({ totalPlatformCents: 0, guestPlatformLineCents: 0 });
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("defaults feeHandling to HostAbsorbs", () => {
|
|
387
|
+
expect(
|
|
388
|
+
splitBashPlatformFeeForTicketCheckout({
|
|
389
|
+
grossCents: 10000,
|
|
390
|
+
platformFeeEnabled: true,
|
|
391
|
+
platformFeeRate: 0.03,
|
|
392
|
+
feeHandling: null,
|
|
393
|
+
bashPassWaivesGuestFee: false,
|
|
394
|
+
})
|
|
395
|
+
).toEqual({ totalPlatformCents: 300, guestPlatformLineCents: 0 });
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
340
399
|
describe("BashPass tier and monthly cap", () => {
|
|
341
400
|
const future = new Date(Date.now() + 86400000 * 30);
|
|
342
401
|
const past = new Date(Date.now() - 86400000);
|
|
@@ -18,12 +18,18 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Bugs in any of these route door-scanners to the wrong page (= entry chaos).
|
|
20
20
|
*/
|
|
21
|
+
import QRCode from "qrcode";
|
|
22
|
+
|
|
21
23
|
import {
|
|
22
24
|
bashEventPrismaCheckoutQrCodeUrl,
|
|
23
25
|
bashEventQrCodeUrl,
|
|
24
26
|
createQrCodeImage,
|
|
25
27
|
} from "../qrCodeUtils.js";
|
|
26
28
|
|
|
29
|
+
jest.mock("qrcode", () => ({
|
|
30
|
+
toDataURL: jest.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
27
33
|
// ============================================================================
|
|
28
34
|
// bashEventPrismaCheckoutQrCodeUrl — checkout-id deep link
|
|
29
35
|
// ============================================================================
|
|
@@ -120,22 +126,43 @@ describe("bashEventQrCodeUrl", () => {
|
|
|
120
126
|
// ============================================================================
|
|
121
127
|
|
|
122
128
|
describe("createQrCodeImage", () => {
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
jest.clearAllMocks();
|
|
131
|
+
});
|
|
132
|
+
|
|
123
133
|
it("returns a base64-encoded PNG data URL", async () => {
|
|
134
|
+
const mockDataUrl =
|
|
135
|
+
"data:image/png;base64," +
|
|
136
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==".repeat(
|
|
137
|
+
3
|
|
138
|
+
);
|
|
139
|
+
(QRCode.toDataURL as jest.Mock).mockResolvedValue(mockDataUrl);
|
|
140
|
+
|
|
124
141
|
const dataUrl = await createQrCodeImage(
|
|
125
142
|
"https://bash.app/ticket-details/evt_1/ticket/abc"
|
|
126
143
|
);
|
|
127
144
|
expect(dataUrl).toMatch(/^data:image\/png;base64,/);
|
|
128
|
-
// Sanity: a real QR image will have *meaningful* base64 length, not a few bytes.
|
|
129
145
|
expect(dataUrl.length).toBeGreaterThan(200);
|
|
146
|
+
expect(QRCode.toDataURL).toHaveBeenCalledWith(
|
|
147
|
+
"https://bash.app/ticket-details/evt_1/ticket/abc",
|
|
148
|
+
{ errorCorrectionLevel: "high" }
|
|
149
|
+
);
|
|
130
150
|
});
|
|
131
151
|
|
|
132
152
|
it("produces a deterministic PNG for the same input (no nonce / timestamp leakage)", async () => {
|
|
153
|
+
const mockDataUrl = "data:image/png;base64,same-payload";
|
|
154
|
+
(QRCode.toDataURL as jest.Mock).mockResolvedValue(mockDataUrl);
|
|
155
|
+
|
|
133
156
|
const a = await createQrCodeImage("ticket://abc");
|
|
134
157
|
const b = await createQrCodeImage("ticket://abc");
|
|
135
158
|
expect(a).toBe(b);
|
|
136
159
|
});
|
|
137
160
|
|
|
138
161
|
it("produces DIFFERENT output for different payloads", async () => {
|
|
162
|
+
(QRCode.toDataURL as jest.Mock)
|
|
163
|
+
.mockResolvedValueOnce("data:image/png;base64,abc")
|
|
164
|
+
.mockResolvedValueOnce("data:image/png;base64,xyz");
|
|
165
|
+
|
|
139
166
|
const a = await createQrCodeImage("ticket://abc");
|
|
140
167
|
const b = await createQrCodeImage("ticket://xyz");
|
|
141
168
|
expect(a).not.toBe(b);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
calculateAverageRating,
|
|
4
|
+
generateReviewStats,
|
|
5
|
+
formatRatingWithStars,
|
|
6
|
+
isEligibleToReview,
|
|
7
|
+
} from "../reviewUtils.js";
|
|
8
|
+
import type { Review } from "../../definitions.js";
|
|
9
|
+
|
|
10
|
+
const reviews: Review[] = [
|
|
11
|
+
{ rating: 5, id: "1" } as Review,
|
|
12
|
+
{ rating: 4, id: "2" } as Review,
|
|
13
|
+
{ rating: 3, id: "3" } as Review,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("reviewUtils", () => {
|
|
17
|
+
describe("calculateAverageRating", () => {
|
|
18
|
+
test("returns 0 for empty list", () => {
|
|
19
|
+
expect(calculateAverageRating([])).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("averages ratings to one decimal", () => {
|
|
23
|
+
expect(calculateAverageRating(reviews)).toBe(4);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("generateReviewStats", () => {
|
|
28
|
+
test("builds distribution and totals", () => {
|
|
29
|
+
expect(generateReviewStats(reviews)).toEqual({
|
|
30
|
+
averageRating: 4,
|
|
31
|
+
totalRatings: 3,
|
|
32
|
+
ratings: { 1: 0, 2: 0, 3: 1, 4: 1, 5: 1 },
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("ignores out-of-range ratings", () => {
|
|
37
|
+
const mixed = [{ rating: 6, id: "x" } as Review, { rating: 0, id: "y" } as Review];
|
|
38
|
+
expect(generateReviewStats(mixed).ratings).toEqual({ 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("formatRatingWithStars", () => {
|
|
43
|
+
test("includes full stars and half star", () => {
|
|
44
|
+
expect(formatRatingWithStars(4.5)).toBe("4.5 ★★★★½");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("rounds down fractional stars below 0.5", () => {
|
|
48
|
+
expect(formatRatingWithStars(3.2)).toBe("3.2 ★★★");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("isEligibleToReview", () => {
|
|
53
|
+
test("allows attendee who has not reviewed and is not the host", () => {
|
|
54
|
+
expect(isEligibleToReview("user-1", "event-1", "host-1", [], ["event-1"])).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("blocks host, non-attendees, and duplicate reviews", () => {
|
|
58
|
+
expect(isEligibleToReview("host-1", "event-1", "host-1", [], ["event-1"])).toBe(false);
|
|
59
|
+
expect(isEligibleToReview("user-1", "event-1", "host-1", [], [])).toBe(false);
|
|
60
|
+
expect(isEligibleToReview("user-1", "event-1", "host-1", ["event-1"], ["event-1"])).toBe(
|
|
61
|
+
false
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
BASH_DETAIL_URL,
|
|
4
|
+
BASH_EVENT_WIZARD_HOW_MAJOR_STEP,
|
|
5
|
+
BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT,
|
|
6
|
+
BASH_EVENT_WIZARD_PATH_PREFIX,
|
|
7
|
+
} from "../../definitions.js";
|
|
8
|
+
import {
|
|
9
|
+
generateSlug,
|
|
10
|
+
generateUniqueSlug,
|
|
11
|
+
generateBashDetailUrl,
|
|
12
|
+
getPublicBashDetailPath,
|
|
13
|
+
getBashEventWizardStepPath,
|
|
14
|
+
getBashEventWizardPayoutStepPath,
|
|
15
|
+
parseBashUrlParams,
|
|
16
|
+
validateSlug,
|
|
17
|
+
} from "../slugUtils.js";
|
|
18
|
+
|
|
19
|
+
describe("generateSlug", () => {
|
|
20
|
+
test("lowercases and slugifies a title", () => {
|
|
21
|
+
expect(generateSlug("Summer Bash 2026!")).toBe("summer-bash-2026");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("removes special characters", () => {
|
|
25
|
+
expect(generateSlug("Hello *World*")).toBe("hello-world");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("handles empty string", () => {
|
|
29
|
+
expect(generateSlug("")).toBe("");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("generateUniqueSlug", () => {
|
|
34
|
+
test("returns base slug when available", async () => {
|
|
35
|
+
const checkSlugExists = async () => false;
|
|
36
|
+
await expect(generateUniqueSlug("My Event", checkSlugExists)).resolves.toBe("my-event");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("appends counter when slug exists", async () => {
|
|
40
|
+
const taken = new Set(["my-event", "my-event-1"]);
|
|
41
|
+
const checkSlugExists = async (slug: string) => taken.has(slug);
|
|
42
|
+
await expect(generateUniqueSlug("My Event", checkSlugExists)).resolves.toBe("my-event-2");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("passes excludeId to checkSlugExists", async () => {
|
|
46
|
+
const calls: Array<{ slug: string; excludeId?: string }> = [];
|
|
47
|
+
const checkSlugExists = async (slug: string, excludeId?: string) => {
|
|
48
|
+
calls.push({ slug, excludeId });
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
51
|
+
await generateUniqueSlug("My Event", checkSlugExists, "event-123");
|
|
52
|
+
expect(calls).toEqual([{ slug: "my-event", excludeId: "event-123" }]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("generateBashDetailUrl", () => {
|
|
57
|
+
test("builds id-slug path", () => {
|
|
58
|
+
expect(generateBashDetailUrl("abc123", "summer-bash")).toBe("/bash/abc123-summer-bash");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("getPublicBashDetailPath", () => {
|
|
63
|
+
test("uses slug path when slug is present", () => {
|
|
64
|
+
expect(getPublicBashDetailPath("abc123", "summer-bash")).toBe(
|
|
65
|
+
"/bash/abc123-summer-bash"
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("trims slug before use", () => {
|
|
70
|
+
expect(getPublicBashDetailPath("abc123", " summer-bash ")).toBe(
|
|
71
|
+
"/bash/abc123-summer-bash"
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("falls back to id-only legacy path when slug missing", () => {
|
|
76
|
+
expect(getPublicBashDetailPath("abc123", null)).toBe(`${BASH_DETAIL_URL}/abc123`);
|
|
77
|
+
expect(getPublicBashDetailPath("abc123", undefined)).toBe(`${BASH_DETAIL_URL}/abc123`);
|
|
78
|
+
expect(getPublicBashDetailPath("abc123", " ")).toBe(`${BASH_DETAIL_URL}/abc123`);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("getBashEventWizardStepPath", () => {
|
|
83
|
+
test("builds wizard step path", () => {
|
|
84
|
+
expect(getBashEventWizardStepPath("evt-1", 3, 2)).toBe(
|
|
85
|
+
`${BASH_EVENT_WIZARD_PATH_PREFIX}/evt-1/3/2`
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("getBashEventWizardPayoutStepPath", () => {
|
|
91
|
+
test("returns How → Payout step path", () => {
|
|
92
|
+
expect(getBashEventWizardPayoutStepPath("evt-1")).toBe(
|
|
93
|
+
`${BASH_EVENT_WIZARD_PATH_PREFIX}/evt-1/${BASH_EVENT_WIZARD_HOW_MAJOR_STEP}/${BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT}`
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("parseBashUrlParams", () => {
|
|
99
|
+
test("parses id-slug format", () => {
|
|
100
|
+
expect(parseBashUrlParams("abc123-summer-bash")).toEqual({
|
|
101
|
+
id: "abc123",
|
|
102
|
+
slug: "summer-bash",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("parses id-only legacy format", () => {
|
|
107
|
+
expect(parseBashUrlParams("abc123")).toEqual({ id: "abc123", slug: null });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns null for empty param", () => {
|
|
111
|
+
expect(parseBashUrlParams("")).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("returns null when id portion is empty", () => {
|
|
115
|
+
expect(parseBashUrlParams("-slug-only")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns slug null when trailing dash has no slug", () => {
|
|
119
|
+
expect(parseBashUrlParams("abc123-")).toEqual({ id: "abc123", slug: null });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("uses first dash as separator only", () => {
|
|
123
|
+
expect(parseBashUrlParams("abc-my-cool-event")).toEqual({
|
|
124
|
+
id: "abc",
|
|
125
|
+
slug: "my-cool-event",
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("validateSlug", () => {
|
|
131
|
+
test.each([
|
|
132
|
+
["valid-slug", true],
|
|
133
|
+
["abc123", true],
|
|
134
|
+
["a-b-c", true],
|
|
135
|
+
["UPPERCASE", false],
|
|
136
|
+
["has spaces", false],
|
|
137
|
+
["has_underscore", false],
|
|
138
|
+
["special!", false],
|
|
139
|
+
["", false],
|
|
140
|
+
])("validateSlug(%s) → %s", (slug, expected) => {
|
|
141
|
+
expect(validateSlug(slug)).toBe(expected);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tier sort ordering.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "@jest/globals";
|
|
6
|
+
import { sortTiers } from "../sortUtils.js";
|
|
7
|
+
|
|
8
|
+
describe("sortUtils", () => {
|
|
9
|
+
describe("sortTiers", () => {
|
|
10
|
+
test("returns 0 when sortOrder matches", () => {
|
|
11
|
+
expect(sortTiers({ sortOrder: 2 }, { sortOrder: 2 })).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("sorts by sortOrder ascending", () => {
|
|
15
|
+
expect(sortTiers({ sortOrder: 1 }, { sortOrder: 3 })).toBeLessThan(0);
|
|
16
|
+
expect(sortTiers({ sortOrder: 5 }, { sortOrder: 2 })).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("falls back to price when sortOrder absent", () => {
|
|
20
|
+
expect(sortTiers({ price: 10 }, { price: 20 })).toBeLessThan(0);
|
|
21
|
+
expect(sortTiers({ price: 20 }, { price: 10 })).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("empty values sort before defined values", () => {
|
|
25
|
+
expect(sortTiers({}, { sortOrder: 1 })).toBeLessThan(0);
|
|
26
|
+
expect(sortTiers({ sortOrder: 1 }, {})).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("returns 0 when both tiers lack comparable fields", () => {
|
|
30
|
+
expect(sortTiers({}, {})).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { formatString } from "../stringUtils.js";
|
|
3
|
+
|
|
4
|
+
describe("formatString", () => {
|
|
5
|
+
test("replaces placeholders with string values", () => {
|
|
6
|
+
expect(formatString("Hello ${name}!", { name: "World" })).toBe("Hello World!");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("replaces placeholders with numeric values", () => {
|
|
10
|
+
expect(formatString("Count: ${count}", { count: 42 })).toBe("Count: 42");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("replaces multiple placeholders", () => {
|
|
14
|
+
expect(
|
|
15
|
+
formatString("${greeting}, ${name}!", { greeting: "Hi", name: "Alex" })
|
|
16
|
+
).toBe("Hi, Alex!");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("uses empty string for missing keys", () => {
|
|
20
|
+
expect(formatString("Hello ${name}!", {})).toBe("Hello !");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("leaves text without placeholders unchanged", () => {
|
|
24
|
+
expect(formatString("No placeholders here", { unused: "x" })).toBe(
|
|
25
|
+
"No placeholders here"
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("handles empty template", () => {
|
|
30
|
+
expect(formatString("", { key: "value" })).toBe("");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("only matches word-character keys", () => {
|
|
34
|
+
expect(formatString("${key_1} ${key2}", { key_1: "a", key2: "b" })).toBe("a b");
|
|
35
|
+
expect(formatString("${key-name}", {})).toBe("${key-name}");
|
|
36
|
+
});
|
|
37
|
+
});
|