feat: improve events browsing filters and calendar
This commit is contained in:
151
tests/event-catalog.test.mjs
Normal file
151
tests/event-catalog.test.mjs
Normal file
@@ -0,0 +1,151 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
EVENT_PAGE_SIZE,
|
||||
getEventLocationLabel,
|
||||
getPriceCeiling,
|
||||
parseEventCatalogParams,
|
||||
serializeEventCatalogParams,
|
||||
} from "../src/lib/event-catalog.ts";
|
||||
import {
|
||||
getEventBrowseLabel,
|
||||
getEventRegistrationMessage,
|
||||
} from "../src/lib/event-ui.ts";
|
||||
|
||||
const now = Date.parse("2026-09-30T12:00:00.000Z");
|
||||
|
||||
function event(overrides = {}) {
|
||||
return {
|
||||
status: "published",
|
||||
event_type: "on_site",
|
||||
address: "",
|
||||
location: null,
|
||||
start_time: "2026-10-01T12:00:00.000Z",
|
||||
end_time: "2026-10-01T14:00:00.000Z",
|
||||
registration_start_date: "2026-09-29T12:00:00.000Z",
|
||||
registration_end_date: "2026-10-01T12:00:00.000Z",
|
||||
capacity: 20,
|
||||
registration_count: 8,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("uses the agreed Persian status labels for completed, open, and upcoming events", () => {
|
||||
assert.equal(getEventBrowseLabel(event({ status: "completed" }), now), "برگزار شده");
|
||||
assert.equal(getEventBrowseLabel(event(), now), "درحال برگزاری");
|
||||
assert.equal(
|
||||
getEventBrowseLabel(event({ registration_start_date: "2026-10-01T12:00:00.000Z" }), now),
|
||||
"بهزودی",
|
||||
);
|
||||
assert.equal(
|
||||
getEventBrowseLabel(event({ capacity: 8 }), now),
|
||||
"بهزودی",
|
||||
);
|
||||
});
|
||||
|
||||
test("uses a distinct registration message for not started, closed, full, and completed events", () => {
|
||||
assert.equal(
|
||||
getEventRegistrationMessage(event({ registration_start_date: "2026-10-01T12:00:00.000Z" }), now),
|
||||
"ثبتنام هنوز آغاز نشده است",
|
||||
);
|
||||
assert.equal(
|
||||
getEventRegistrationMessage(event({ registration_end_date: "2026-09-29T11:59:00.000Z" }), now),
|
||||
"زمان ثبتنام به پایان رسیده است",
|
||||
);
|
||||
assert.equal(
|
||||
getEventRegistrationMessage(event({ capacity: 8 }), now),
|
||||
"ظرفیت ثبتنام تکمیل شده است",
|
||||
);
|
||||
assert.equal(
|
||||
getEventRegistrationMessage(event({ status: "completed" }), now),
|
||||
"این رویداد برگزار شده است",
|
||||
);
|
||||
});
|
||||
|
||||
test("rounds the global price maximum up to the next complete million", () => {
|
||||
assert.equal(getPriceCeiling(650000), 1000000);
|
||||
assert.equal(getPriceCeiling(4300000), 5000000);
|
||||
assert.equal(getPriceCeiling(5000000), 5000000);
|
||||
assert.equal(getPriceCeiling(0), 0);
|
||||
});
|
||||
|
||||
test("always provides a location label and applies the requested fallbacks", () => {
|
||||
assert.equal(getEventLocationLabel(event({ event_type: "online" })), "برگزاری آنلاین");
|
||||
assert.equal(getEventLocationLabel(event({ event_type: "online", address: "گوگلمیت" })), "برگزاری آنلاین");
|
||||
assert.equal(getEventLocationLabel(event()), "محل برگزاری اعلام نشده");
|
||||
assert.equal(getEventLocationLabel(event({ address: "ساختمان دانشکده" })), "ساختمان دانشکده");
|
||||
});
|
||||
|
||||
test("round-trips shareable catalog filters and defaults to 12 items per page", () => {
|
||||
const filters = parseEventCatalogParams(new URLSearchParams(
|
||||
"search=برنامه%20نویسی&state=upcoming&event_type=hybrid&date_from=2026-09-20&date_to=2026-09-30&price_min=100000&price_max=500000&page=3",
|
||||
), 1000000);
|
||||
|
||||
assert.deepEqual(filters, {
|
||||
search: "برنامه نویسی",
|
||||
state: "upcoming",
|
||||
eventType: "hybrid",
|
||||
dateFrom: "2026-09-20",
|
||||
dateTo: "2026-09-30",
|
||||
priceMin: 100000,
|
||||
priceMax: 500000,
|
||||
freeOnly: false,
|
||||
page: 3,
|
||||
});
|
||||
assert.deepEqual(
|
||||
parseEventCatalogParams(serializeEventCatalogParams(filters, 1000000), 1000000),
|
||||
filters,
|
||||
);
|
||||
assert.equal(
|
||||
serializeEventCatalogParams({
|
||||
search: "",
|
||||
state: "all",
|
||||
eventType: "all",
|
||||
dateFrom: "",
|
||||
dateTo: "",
|
||||
priceMin: 0,
|
||||
priceMax: 1000000,
|
||||
freeOnly: false,
|
||||
page: 1,
|
||||
}, 1000000).toString(),
|
||||
"",
|
||||
);
|
||||
assert.equal(EVENT_PAGE_SIZE, 12);
|
||||
});
|
||||
|
||||
test("omits price bounds from the URL when free-only is enabled", () => {
|
||||
const params = serializeEventCatalogParams({
|
||||
search: "",
|
||||
state: "upcoming",
|
||||
eventType: "all",
|
||||
dateFrom: "",
|
||||
dateTo: "",
|
||||
priceMin: 350000,
|
||||
priceMax: 1000000,
|
||||
freeOnly: true,
|
||||
page: 1,
|
||||
}, 1000000);
|
||||
|
||||
assert.equal(params.get("free_only"), "true");
|
||||
assert.equal(params.has("price_min"), false);
|
||||
assert.equal(params.has("price_max"), false);
|
||||
});
|
||||
|
||||
test("ignores invalid or reversed query values and uses safe catalog defaults", () => {
|
||||
assert.deepEqual(
|
||||
parseEventCatalogParams(new URLSearchParams(
|
||||
"state=unknown&event_type=other&date_from=2026-10-02&date_to=2026-10-01&price_min=-1&price_max=nope&page=0",
|
||||
), 500000),
|
||||
{
|
||||
search: "",
|
||||
state: "all",
|
||||
eventType: "all",
|
||||
dateFrom: "",
|
||||
dateTo: "",
|
||||
priceMin: 0,
|
||||
priceMax: 500000,
|
||||
freeOnly: false,
|
||||
page: 1,
|
||||
},
|
||||
);
|
||||
});
|
||||
108
tests/event-ui.test.mjs
Normal file
108
tests/event-ui.test.mjs
Normal file
@@ -0,0 +1,108 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
getEventCardState,
|
||||
getEventRegistrationState,
|
||||
classifyRegistrationError,
|
||||
getPaymentOutcome,
|
||||
getRegistrationOutcome,
|
||||
getEventPath,
|
||||
} from "../src/lib/event-ui.ts";
|
||||
|
||||
const now = Date.parse("2026-09-29T12:00:00.000Z");
|
||||
|
||||
function event(overrides = {}) {
|
||||
return {
|
||||
status: "published",
|
||||
start_time: "2026-10-01T12:00:00.000Z",
|
||||
end_time: "2026-10-01T14:00:00.000Z",
|
||||
registration_start_date: null,
|
||||
registration_end_date: "2026-09-30T12:00:00.000Z",
|
||||
capacity: 20,
|
||||
registration_count: 8,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("treats a missing capacity as unlimited", () => {
|
||||
assert.deepEqual(
|
||||
getEventRegistrationState(event({ capacity: null }), now),
|
||||
{ state: "open", remaining: null },
|
||||
);
|
||||
});
|
||||
|
||||
test("marks an event full when confirmed registrations reach capacity", () => {
|
||||
assert.deepEqual(
|
||||
getEventRegistrationState(event({ capacity: 8 }), now),
|
||||
{ state: "full", remaining: 0 },
|
||||
);
|
||||
});
|
||||
|
||||
test("uses inclusive registration window boundaries like the backend", () => {
|
||||
assert.equal(
|
||||
getEventRegistrationState(event({ registration_start_date: new Date(now).toISOString() }), now).state,
|
||||
"open",
|
||||
);
|
||||
assert.equal(
|
||||
getEventRegistrationState(event({ registration_end_date: new Date(now).toISOString() }), now).state,
|
||||
"open",
|
||||
);
|
||||
});
|
||||
|
||||
test("distinguishes unpublished, not-yet-open, and ended registration", () => {
|
||||
assert.equal(getEventRegistrationState(event({ status: "completed" }), now).state, "unavailable");
|
||||
assert.equal(
|
||||
getEventRegistrationState(event({ registration_start_date: "2026-09-30T12:00:00.000Z" }), now).state,
|
||||
"not_started",
|
||||
);
|
||||
assert.equal(
|
||||
getEventRegistrationState(event({ registration_end_date: "2026-09-29T11:59:59.000Z" }), now).state,
|
||||
"closed",
|
||||
);
|
||||
});
|
||||
|
||||
test("prioritizes an elapsed registration deadline over a future opening date", () => {
|
||||
assert.equal(
|
||||
getEventRegistrationState(event({
|
||||
registration_start_date: "2026-09-30T12:00:00.000Z",
|
||||
registration_end_date: "2026-09-28T12:00:00.000Z",
|
||||
}), now).state,
|
||||
"closed",
|
||||
);
|
||||
});
|
||||
|
||||
test("labels completed events separately from registration availability", () => {
|
||||
assert.equal(getEventCardState(event({ status: "completed" }), now), "past");
|
||||
assert.equal(getEventCardState(event(), now), "upcoming");
|
||||
assert.equal(getEventCardState(event({ capacity: 8 }), now), "full");
|
||||
});
|
||||
|
||||
test("classifies backend registration errors including Persian duplicate responses", () => {
|
||||
assert.equal(classifyRegistrationError("شما قبلا در این ایونت ثبتنام کردهاید."), "already_registered");
|
||||
assert.equal(classifyRegistrationError("زمان ثبتنام هنوز آغاز نشده است"), "not_started");
|
||||
assert.equal(classifyRegistrationError("مهلت ثبتنام به پایان رسیدهاست"), "closed");
|
||||
assert.equal(classifyRegistrationError("ظرفیت شرکتکنندگان تکمیل است"), "full");
|
||||
assert.equal(classifyRegistrationError("network error"), "unknown");
|
||||
});
|
||||
|
||||
test("keeps callback outcomes distinct and never treats unknown as paid", () => {
|
||||
assert.equal(getPaymentOutcome("success"), "paid");
|
||||
assert.equal(getPaymentOutcome("canceled"), "canceled");
|
||||
assert.equal(getPaymentOutcome("failed"), "failed");
|
||||
assert.equal(getPaymentOutcome("pending"), "pending");
|
||||
assert.equal(getPaymentOutcome("unexpected"), "unknown");
|
||||
});
|
||||
|
||||
test("distinguishes confirmed, pending, canceled, and unknown registrations", () => {
|
||||
assert.equal(getRegistrationOutcome("confirmed"), "confirmed");
|
||||
assert.equal(getRegistrationOutcome("attended"), "confirmed");
|
||||
assert.equal(getRegistrationOutcome("pending"), "pending");
|
||||
assert.equal(getRegistrationOutcome("cancelled"), "canceled");
|
||||
assert.equal(getRegistrationOutcome("other"), "unknown");
|
||||
});
|
||||
|
||||
test("uses the event id as a detail route when the event has no slug", () => {
|
||||
assert.equal(getEventPath({ id: 42, slug: "" }), "/events/42");
|
||||
assert.equal(getEventPath({ id: 42, slug: " " }), "/events/42");
|
||||
assert.equal(getEventPath({ id: 42, slug: "event-title" }), "/events/event-title");
|
||||
});
|
||||
Reference in New Issue
Block a user