@tribe-nest/forge 3.60.0 → 3.63.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/package.json +5 -2
- package/src/contexts/CartContext.tsx +34 -1
- package/src/contexts/PublicAuthContext.tsx +15 -3
- package/src/data/queries/_tests/metaSignals.spec.tsx +130 -0
- package/src/data/queries/useAccount.ts +33 -2
- package/src/data/queries/useAnalytics.ts +6 -6
- package/src/data/queries/useCallChat.ts +2 -2
- package/src/data/queries/useChat.ts +10 -1
- package/src/data/queries/useCheckouts.ts +2 -1
- package/src/data/queries/useCoachingAvailability.ts +10 -1
- package/src/data/queries/useCommunities.ts +2 -0
- package/src/data/queries/useCommunity.ts +362 -18
- package/src/data/queries/useCommunityAgent.ts +247 -0
- package/src/data/queries/useCommunityCalendar.ts +150 -0
- package/src/data/queries/useCommunityClassroom.ts +75 -0
- package/src/data/queries/useCommunityMembers.ts +108 -0
- package/src/data/queries/useCommunityModeration.ts +327 -0
- package/src/data/queries/useCommunityWiki.ts +137 -0
- package/src/data/queries/useCourses.ts +2 -0
- package/src/data/queries/useEmailSignIn.ts +79 -0
- package/src/data/queries/useEvents.ts +6 -1
- package/src/data/queries/useForms.ts +5 -1
- package/src/data/queries/useLeadMagnet.ts +6 -2
- package/src/data/queries/useMyBookings.ts +24 -1
- package/src/data/queries/useMyTickets.ts +24 -1
- package/src/data/queries/useNotifications.ts +6 -2
- package/src/data/queries/useOrders.ts +2 -1
- package/src/data/queries/usePWA.ts +111 -35
- package/src/data/queries/useSiteApp.ts +130 -0
- package/src/data/queries/useSubscriptions.ts +6 -1
- package/src/i18n/de.json +746 -1
- package/src/i18n/en.json +746 -1
- package/src/index.ts +20 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/seo/_tests/seo.spec.ts +326 -0
- package/src/seo/applySeoToHead.ts +98 -0
- package/src/seo/index.ts +9 -0
- package/src/seo/resolve.ts +211 -0
- package/src/seo/seoHead.ts +61 -0
- package/src/seo/siteSeoHead.ts +97 -0
- package/src/seo/types.ts +99 -0
- package/src/server/_tests/siteBootstrap.spec.ts +65 -0
- package/src/server/index.ts +33 -4
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +134 -34
- package/src/ui/analytics/PageMetaPixel.tsx +23 -8
- package/src/ui/analytics/_tests/ForgeAnalytics.meta.spec.tsx +129 -0
- package/src/ui/analytics/_tests/ForgeAnalytics.webVitals.spec.tsx +129 -0
- package/src/ui/analytics/_tests/metaCommerceEvents.spec.tsx +159 -0
- package/src/ui/analytics/useMetaEvents.ts +78 -0
- package/src/ui/headless/_tests/communityAgent.spec.tsx +519 -0
- package/src/ui/headless/_tests/communityCalendar.spec.tsx +195 -0
- package/src/ui/headless/_tests/communityClassroom.spec.tsx +120 -0
- package/src/ui/headless/_tests/communityHarness.tsx +10 -3
- package/src/ui/headless/_tests/communityMembers.spec.tsx +316 -0
- package/src/ui/headless/_tests/communityPoll.spec.tsx +389 -0
- package/src/ui/headless/_tests/memberHomeBar.spec.ts +84 -0
- package/src/ui/headless/_tests/siteAppBar.spec.ts +83 -0
- package/src/ui/headless/_tests/siteAppHarness.ts +19 -0
- package/src/ui/headless/_tests/siteAppHome.spec.ts +226 -0
- package/src/ui/headless/_tests/siteAppPrompts.spec.ts +185 -0
- package/src/ui/headless/_tests/useCommunityJoin.spec.tsx +228 -14
- package/src/ui/headless/_tests/useCommunityMembership.spec.tsx +29 -1
- package/src/ui/headless/_tests/useCommunityModeration.spec.tsx +420 -0
- package/src/ui/headless/_tests/useCommunityReads.spec.tsx +11 -2
- package/src/ui/headless/_tests/useCommunityWiki.spec.tsx +224 -0
- package/src/ui/headless/_tests/useSiteApp.spec.tsx +52 -0
- package/src/ui/headless/_tests/wikiMediaImage.spec.tsx +132 -0
- package/src/ui/headless/auth/useEmailCodeSignIn.ts +186 -0
- package/src/ui/headless/auth/useSignupForm.ts +20 -1
- package/src/ui/headless/chat/useChatAttachmentUpload.ts +9 -6
- package/src/ui/headless/chat/useChatRoom.ts +35 -1
- package/src/ui/headless/communities/index.ts +41 -0
- package/src/ui/headless/communities/useCommunityCalendar.ts +156 -0
- package/src/ui/headless/communities/useCommunityClassroom.ts +84 -0
- package/src/ui/headless/communities/useCommunityHome.ts +3 -0
- package/src/ui/headless/communities/useCommunityJoin.ts +73 -23
- package/src/ui/headless/communities/useCommunityMembership.ts +48 -2
- package/src/ui/headless/communities/useCommunityWiki.ts +266 -0
- package/src/ui/headless/community/communityPoll.ts +297 -0
- package/src/ui/headless/community/countries.ts +123 -0
- package/src/ui/headless/community/index.ts +81 -1
- package/src/ui/headless/community/linkPreview.ts +71 -0
- package/src/ui/headless/community/richLexical.ts +27 -2
- package/src/ui/headless/community/useCommunityAgent.ts +285 -0
- package/src/ui/headless/community/useCommunityMembers.ts +175 -0
- package/src/ui/headless/community/useCommunityModeration.ts +300 -0
- package/src/ui/headless/community/useCommunitySpaceFeed.ts +22 -4
- package/src/ui/headless/community/useCommunityThread.ts +16 -3
- package/src/ui/headless/community/useMentionComposer.ts +17 -9
- package/src/ui/headless/community/useRichMentionComposer.ts +21 -5
- package/src/ui/headless/downloads/index.ts +8 -0
- package/src/ui/headless/downloads/useDownloadPage.ts +134 -0
- package/src/ui/headless/index.ts +13 -0
- package/src/ui/headless/memberHome/index.ts +10 -0
- package/src/ui/headless/memberHome/memberHomeBar.ts +91 -0
- package/src/ui/headless/memberHome/useMemberHomeNav.ts +56 -18
- package/src/ui/headless/portal/index.ts +57 -0
- package/src/ui/headless/portal/usePortal.ts +274 -0
- package/src/ui/headless/portal/usePortalFiles.ts +68 -0
- package/src/ui/headless/portal/usePortalMessages.ts +189 -0
- package/src/ui/headless/portal/usePortalPreview.ts +142 -0
- package/src/ui/headless/siteApp/appBar.ts +102 -0
- package/src/ui/headless/siteApp/appHome.ts +344 -0
- package/src/ui/headless/siteApp/appPrompts.ts +229 -0
- package/src/ui/headless/siteApp/index.ts +58 -0
- package/src/ui/headless/siteApp/useAppHome.ts +210 -0
- package/src/ui/headless/siteApp/useAppInstallRequest.ts +16 -0
- package/src/ui/headless/work/index.ts +2 -0
- package/src/ui/headless/work/useWorkPortal.ts +37 -26
- package/src/ui/index.ts +11 -0
- package/src/ui/styled/AccountDashboard.tsx +712 -352
- package/src/ui/styled/AiAgentWidget.tsx +13 -1
- package/src/ui/styled/ChatRoom.tsx +126 -16
- package/src/ui/styled/Checkout.tsx +8 -0
- package/src/ui/styled/CheckoutConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingConfirmation.tsx +8 -0
- package/src/ui/styled/CoachingDetail.tsx +2 -0
- package/src/ui/styled/CourseConfirmation.tsx +5 -0
- package/src/ui/styled/CourseDetail.tsx +2 -0
- package/src/ui/styled/DownloadPage.tsx +263 -0
- package/src/ui/styled/EventConfirmation.tsx +10 -0
- package/src/ui/styled/EventDetail.tsx +10 -0
- package/src/ui/styled/FilmLibrary.tsx +22 -9
- package/src/ui/styled/MusicLinkPage.tsx +5 -5
- package/src/ui/styled/ProductDetail.tsx +12 -0
- package/src/ui/styled/PwaRegistration.tsx +5 -1
- package/src/ui/styled/SignupForm.tsx +18 -3
- package/src/ui/styled/_tests/AccountDashboardBookingCall.spec.tsx +5 -5
- package/src/ui/styled/_tests/AccountNotifications.spec.tsx +135 -0
- package/src/ui/styled/_tests/DownloadPage.spec.tsx +68 -0
- package/src/ui/styled/app/AppBar.tsx +298 -0
- package/src/ui/styled/app/AppHome.tsx +1139 -0
- package/src/ui/styled/app/AppHomeMark.tsx +66 -0
- package/src/ui/styled/app/AppPrompts.tsx +464 -0
- package/src/ui/styled/app/_tests/AppBar.spec.tsx +96 -0
- package/src/ui/styled/app/_tests/AppHome.spec.tsx +209 -0
- package/src/ui/styled/app/_tests/AppPrompts.spec.tsx +232 -0
- package/src/ui/styled/app/index.ts +7 -0
- package/src/ui/styled/chat/ChatThreadPanel.tsx +29 -4
- package/src/ui/styled/chat/MessageBubble.tsx +43 -6
- package/src/ui/styled/communities/CommunityAbout.tsx +6 -1
- package/src/ui/styled/communities/CommunityCalendar.tsx +484 -0
- package/src/ui/styled/communities/CommunityClassroom.tsx +302 -0
- package/src/ui/styled/communities/CommunityHome.tsx +134 -11
- package/src/ui/styled/communities/CommunityJoinSheet.tsx +119 -19
- package/src/ui/styled/communities/CommunityMembershipMenu.tsx +168 -52
- package/src/ui/styled/communities/CommunityPublicWiki.tsx +75 -0
- package/src/ui/styled/communities/CommunityWiki.tsx +289 -0
- package/src/ui/styled/communities/MyMembershipsList.tsx +31 -5
- package/src/ui/styled/communities/_tests/CommunityAppHome.spec.tsx +92 -0
- package/src/ui/styled/communities/_tests/CommunityClassroomCalendar.spec.tsx +453 -0
- package/src/ui/styled/communities/_tests/CommunityMobile.spec.tsx +399 -0
- package/src/ui/styled/communities/_tests/CommunityWiki.spec.tsx +223 -0
- package/src/ui/styled/communities/index.ts +9 -0
- package/src/ui/styled/communities/shared.tsx +39 -11
- package/src/ui/styled/community/CommunityAgent.tsx +286 -0
- package/src/ui/styled/community/CommunityComposer.tsx +266 -15
- package/src/ui/styled/community/CommunityFeed.tsx +50 -1
- package/src/ui/styled/community/CommunityLinkPreview.tsx +159 -0
- package/src/ui/styled/community/CommunityMemberGlobe.tsx +171 -0
- package/src/ui/styled/community/CommunityMemberProfile.tsx +137 -4
- package/src/ui/styled/community/CommunityMembers.tsx +387 -0
- package/src/ui/styled/community/CommunityModeration.tsx +587 -0
- package/src/ui/styled/community/CommunityModerationState.tsx +127 -0
- package/src/ui/styled/community/CommunityModeratorMenu.tsx +317 -0
- package/src/ui/styled/community/CommunityNotifications.tsx +52 -6
- package/src/ui/styled/community/CommunityPollCard.tsx +250 -0
- package/src/ui/styled/community/CommunityPostCard.tsx +110 -34
- package/src/ui/styled/community/CommunityPostDetail.tsx +157 -71
- package/src/ui/styled/community/CommunityRichText.tsx +177 -32
- package/src/ui/styled/community/CommunitySpaces.tsx +49 -14
- package/src/ui/styled/community/_tests/CommunityAgent.spec.tsx +571 -0
- package/src/ui/styled/community/_tests/CommunityLinkPreview.spec.tsx +167 -0
- package/src/ui/styled/community/_tests/CommunityMembers.spec.tsx +137 -0
- package/src/ui/styled/community/_tests/CommunityModeration.spec.tsx +291 -0
- package/src/ui/styled/community/_tests/CommunityPoll.spec.tsx +386 -0
- package/src/ui/styled/community/index.ts +37 -2
- package/src/ui/styled/forge-utilities.css +234 -0
- package/src/ui/styled/members/CallRecordingActions.tsx +182 -0
- package/src/ui/styled/members/MemberCallsCalendar.tsx +13 -158
- package/src/ui/styled/members/MemberHomeLayout.tsx +295 -15
- package/src/ui/styled/members/MemberPostsFeed.tsx +3 -2
- package/src/ui/styled/members/index.ts +8 -0
- package/src/ui/styled/portal/PortalAccountSection.tsx +61 -0
- package/src/ui/styled/portal/PortalActivity.tsx +167 -0
- package/src/ui/styled/portal/PortalBilling.tsx +197 -0
- package/src/ui/styled/portal/PortalFiles.tsx +279 -0
- package/src/ui/styled/portal/PortalHome.tsx +401 -0
- package/src/ui/styled/portal/PortalMessages.tsx +300 -0
- package/src/ui/styled/portal/PortalPreviewBanner.tsx +75 -0
- package/src/ui/styled/portal/PortalProjects.tsx +407 -0
- package/src/ui/styled/portal/PortalShell.tsx +397 -0
- package/src/ui/styled/portal/_tests/PortalAppHome.spec.tsx +90 -0
- package/src/ui/styled/portal/_tests/PortalFiles.spec.tsx +159 -0
- package/src/ui/styled/portal/_tests/PortalHome.spec.tsx +148 -0
- package/src/ui/styled/portal/_tests/PortalMessages.spec.tsx +168 -0
- package/src/ui/styled/portal/_tests/PortalPreviewBanner.spec.tsx +132 -0
- package/src/ui/styled/portal/_tests/portalNav.spec.ts +123 -0
- package/src/ui/styled/portal/index.ts +13 -0
- package/src/ui/styled/portal/portalNav.ts +260 -0
- package/src/ui/styled/portal/shared.tsx +206 -0
- package/src/ui/styled/work/WorkInviteAccept.tsx +228 -168
- package/src/ui/styled/work/WorkPortalProjects.tsx +68 -120
- package/src/ui/styled/work/WorkProjectDocuments.tsx +99 -128
- package/src/ui/styled/work/WorkProjectInvoices.tsx +91 -86
- package/src/ui/styled/work/WorkProjectReport.tsx +11 -17
- package/src/ui/styled/work/WorkReportBody.tsx +223 -248
- package/src/ui/styled/work/WorkTaskAttachments.tsx +23 -29
- package/src/ui/styled/work/WorkTaskComments.tsx +75 -102
- package/src/ui/styled/work/WorkTaskDetail.tsx +48 -59
- package/src/ui/styled/work/WorkTokenReport.tsx +8 -16
- package/src/ui/styled/work/index.ts +1 -1
- package/src/utils/_tests/landing.spec.ts +39 -0
- package/src/utils/_tests/metaEvents.spec.ts +264 -0
- package/src/utils/_tests/structuredDataSeo.spec.ts +247 -0
- package/src/utils/landing.ts +36 -10
- package/src/utils/metaEvents.ts +302 -0
- package/src/utils/structuredData.ts +339 -1
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { ForgeThemeProvider } from "../../../theme/ForgeThemeProvider";
|
|
6
|
+
import { CommunityUpgradeOpenerProvider } from "../../../../contexts/CommunityContext";
|
|
7
|
+
import { CommunityClassroom } from "../CommunityClassroom";
|
|
8
|
+
import { CommunityCalendar } from "../CommunityCalendar";
|
|
9
|
+
import type { CommunityClassroomCourse } from "../../../../data/queries/useCommunityClassroom";
|
|
10
|
+
import type { CommunityCalendarCall, CommunityCalendarEvent } from "../../../../data/queries/useCommunityCalendar";
|
|
11
|
+
import {
|
|
12
|
+
PROFILE_ID,
|
|
13
|
+
account,
|
|
14
|
+
calls,
|
|
15
|
+
get,
|
|
16
|
+
installAdapter,
|
|
17
|
+
restoreAdapter,
|
|
18
|
+
wrapper,
|
|
19
|
+
type Route,
|
|
20
|
+
} from "../../../headless/_tests/communityHarness";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The classroom and calendar tabs' markup: course cards with progress, what
|
|
24
|
+
* opens a locked one and the upgrade; the agenda's days, going and not going,
|
|
25
|
+
* the refused-answer message, Join only while a room is open, the calendar
|
|
26
|
+
* file link, and an event linking to its page (or showing it was cancelled).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
cleanup();
|
|
31
|
+
restoreAdapter();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
35
|
+
|
|
36
|
+
function mount(node: ReactNode) {
|
|
37
|
+
const Wrapper = wrapper({ user: account() });
|
|
38
|
+
return render(
|
|
39
|
+
<Wrapper>
|
|
40
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
41
|
+
</Wrapper>,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CLASSROOM = "/public/communities/fan-club/classroom";
|
|
46
|
+
|
|
47
|
+
const course = (overrides: Partial<CommunityClassroomCourse> = {}): CommunityClassroomCourse => ({
|
|
48
|
+
courseId: "course-1",
|
|
49
|
+
title: "Songwriting 101",
|
|
50
|
+
description: "From idea to demo.",
|
|
51
|
+
slug: "songwriting-101",
|
|
52
|
+
coverUrl: "https://cdn.test/cover.jpg",
|
|
53
|
+
open: true,
|
|
54
|
+
progress: { completed: 3, total: 10, completedAt: null, currentLessonId: "lesson-4" },
|
|
55
|
+
unlockLevel: null,
|
|
56
|
+
unlockTiers: [],
|
|
57
|
+
accessId: "acc-1",
|
|
58
|
+
...overrides,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("CommunityClassroom", () => {
|
|
62
|
+
it("shows an open course with its progress, linking to the course player", async () => {
|
|
63
|
+
installAdapter([get(CLASSROOM, () => [course()])]);
|
|
64
|
+
const went: string[] = [];
|
|
65
|
+
mount(<CommunityClassroom navigate={(href) => went.push(href)} />);
|
|
66
|
+
const card = await screen.findByTestId("community-classroom-course");
|
|
67
|
+
expect(card.getAttribute("data-state")).toBe("in_progress");
|
|
68
|
+
expect(card.textContent).toContain("Songwriting 101");
|
|
69
|
+
expect(within(card).getByTestId("community-classroom-progress").textContent).toBe("3 of 10 lessons");
|
|
70
|
+
expect(within(card).getByRole("progressbar").getAttribute("aria-valuenow")).toBe("30");
|
|
71
|
+
expect(card.getAttribute("href")).toBe("/i/course-access/acc-1");
|
|
72
|
+
fireEvent.click(card);
|
|
73
|
+
expect(went).toEqual(["/i/course-access/acc-1"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("never sends the owner of a held course to its sales page", async () => {
|
|
77
|
+
installAdapter([get(CLASSROOM, () => [course({ accessId: null })])]);
|
|
78
|
+
const went: string[] = [];
|
|
79
|
+
mount(<CommunityClassroom navigate={(href) => went.push(href)} />);
|
|
80
|
+
const card = await screen.findByTestId("community-classroom-course");
|
|
81
|
+
expect(card.tagName).toBe("DIV");
|
|
82
|
+
expect(card.getAttribute("data-state")).toBe("in_progress");
|
|
83
|
+
expect(document.querySelector('a[href="/i/courses/songwriting-101"]')).toBeNull();
|
|
84
|
+
fireEvent.click(card);
|
|
85
|
+
expect(went).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("says Completed once the course is done", async () => {
|
|
89
|
+
installAdapter([
|
|
90
|
+
get(CLASSROOM, () => [
|
|
91
|
+
course({ progress: { completed: 10, total: 10, completedAt: "2026-09-01T00:00:00.000Z", currentLessonId: null } }),
|
|
92
|
+
]),
|
|
93
|
+
]);
|
|
94
|
+
mount(<CommunityClassroom />);
|
|
95
|
+
expect((await screen.findByTestId("community-classroom-progress")).textContent).toBe("Completed");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("shows a level-locked course with its level and no upgrade", async () => {
|
|
99
|
+
installAdapter([get(CLASSROOM, () => [course({ open: false, progress: null, unlockLevel: 4 })])]);
|
|
100
|
+
mount(<CommunityClassroom />);
|
|
101
|
+
const card = await screen.findByTestId("community-classroom-course");
|
|
102
|
+
expect(card.getAttribute("data-state")).toBe("locked");
|
|
103
|
+
expect(card.tagName).toBe("DIV");
|
|
104
|
+
expect(within(card).getByTestId("community-classroom-unlock").textContent).toBe("Unlocks at level 4");
|
|
105
|
+
expect(within(card).queryByTestId("community-classroom-upgrade")).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("shows a tier-locked course with its tiers, upgrading through the community's sheet", async () => {
|
|
109
|
+
installAdapter([
|
|
110
|
+
get(CLASSROOM, () => [
|
|
111
|
+
course({
|
|
112
|
+
open: false,
|
|
113
|
+
progress: null,
|
|
114
|
+
unlockTiers: [
|
|
115
|
+
{ id: "tier-gold", name: "Gold" },
|
|
116
|
+
{ id: "tier-vip", name: "VIP" },
|
|
117
|
+
],
|
|
118
|
+
}),
|
|
119
|
+
]),
|
|
120
|
+
]);
|
|
121
|
+
const opened: Array<string | null | undefined> = [];
|
|
122
|
+
mount(
|
|
123
|
+
<CommunityUpgradeOpenerProvider value={{ openUpgrade: (tierId) => opened.push(tierId) }}>
|
|
124
|
+
<CommunityClassroom />
|
|
125
|
+
</CommunityUpgradeOpenerProvider>,
|
|
126
|
+
);
|
|
127
|
+
const card = await screen.findByTestId("community-classroom-course");
|
|
128
|
+
expect(within(card).getByTestId("community-classroom-unlock").textContent).toBe("Included in Gold, VIP");
|
|
129
|
+
fireEvent.click(within(card).getByTestId("community-classroom-upgrade"));
|
|
130
|
+
expect(opened).toEqual(["tier-gold"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("falls back to the About page with the tier chosen when no sheet is on hand", async () => {
|
|
134
|
+
installAdapter([
|
|
135
|
+
get(CLASSROOM, () => [course({ open: false, progress: null, unlockTiers: [{ id: "tier-gold", name: "Gold" }] })]),
|
|
136
|
+
]);
|
|
137
|
+
mount(<CommunityClassroom />);
|
|
138
|
+
const upgrade = await screen.findByTestId("community-classroom-upgrade");
|
|
139
|
+
expect(upgrade.getAttribute("href")).toBe("/i/communities/fan-club?tier=tier-gold");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("says so when there are no courses", async () => {
|
|
143
|
+
installAdapter([get(CLASSROOM, () => [])]);
|
|
144
|
+
mount(<CommunityClassroom />);
|
|
145
|
+
expect((await screen.findByTestId("community-classroom-empty")).textContent).toContain("There are no courses here yet.");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("tells a non-member the classroom is for members", async () => {
|
|
149
|
+
installAdapter([
|
|
150
|
+
get(CLASSROOM, () => {
|
|
151
|
+
throw { status: 403, message: "Forbidden" };
|
|
152
|
+
}),
|
|
153
|
+
]);
|
|
154
|
+
mount(<CommunityClassroom />);
|
|
155
|
+
expect((await screen.findByTestId("community-classroom-forbidden")).textContent).toBe(
|
|
156
|
+
"The classroom is for members of this community.",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("says the classroom could not load on a server error", async () => {
|
|
161
|
+
installAdapter([
|
|
162
|
+
get(CLASSROOM, () => {
|
|
163
|
+
throw { status: 500, message: "Boom" };
|
|
164
|
+
}),
|
|
165
|
+
]);
|
|
166
|
+
mount(<CommunityClassroom />);
|
|
167
|
+
expect((await screen.findByRole("alert", undefined, { timeout: 4000 })).textContent).toBe(
|
|
168
|
+
"We couldn't load the classroom. Please try again.",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ── Calendar ─────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const CALENDAR = "/public/communities/fan-club/calendar";
|
|
176
|
+
const at = (day: number, hour: number) => new Date(2026, 8, day, hour, 0, 0).toISOString();
|
|
177
|
+
const NOW = new Date(2026, 8, 15, 12, 0, 0);
|
|
178
|
+
|
|
179
|
+
const call = (overrides: Partial<CommunityCalendarCall> = {}): CommunityCalendarCall => ({
|
|
180
|
+
kind: "call",
|
|
181
|
+
id: "11111111-aaaa-4aaa-8aaa-111111111111",
|
|
182
|
+
title: "Weekly Q&A",
|
|
183
|
+
description: "Bring questions.",
|
|
184
|
+
startsAt: at(16, 18),
|
|
185
|
+
endsAt: at(16, 19),
|
|
186
|
+
timezone: "Europe/Berlin",
|
|
187
|
+
participation: "open",
|
|
188
|
+
role: "attendee",
|
|
189
|
+
audiences: [{ id: "a1", audienceType: "community", audienceId: "community-1", label: "Fan club" }],
|
|
190
|
+
opensAt: at(16, 17),
|
|
191
|
+
closesAt: at(16, 19),
|
|
192
|
+
isOpen: false,
|
|
193
|
+
callPublicId: "pub-1",
|
|
194
|
+
rsvp: { going: 2, notGoing: 1, mine: null },
|
|
195
|
+
icsUrl: "https://api.test/public/calendar/calls/c1.ics",
|
|
196
|
+
recording: null,
|
|
197
|
+
...overrides,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const event = (overrides: Partial<CommunityCalendarEvent> = {}): CommunityCalendarEvent => ({
|
|
201
|
+
kind: "event",
|
|
202
|
+
id: "event-1",
|
|
203
|
+
title: "Album launch",
|
|
204
|
+
description: null,
|
|
205
|
+
slug: "album-launch",
|
|
206
|
+
startsAt: at(18, 20),
|
|
207
|
+
endsAt: null,
|
|
208
|
+
timezone: "Europe/Berlin",
|
|
209
|
+
type: "physical",
|
|
210
|
+
address: { name: "The Club", city: "Berlin" },
|
|
211
|
+
cancelled: false,
|
|
212
|
+
icsUrl: "https://api.test/public/calendar/events/e1.ics",
|
|
213
|
+
...overrides,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const RSVP = `/public/communities/fan-club/calendar/calls/${call().id}/rsvp`;
|
|
217
|
+
const put =
|
|
218
|
+
(path: string, answer: (req: { body: Record<string, unknown> | null }) => unknown): Route =>
|
|
219
|
+
(req) =>
|
|
220
|
+
req.method === "put" && req.url === path ? answer(req) : undefined;
|
|
221
|
+
|
|
222
|
+
describe("CommunityCalendar", () => {
|
|
223
|
+
it("groups the month by day, with what already happened after what is ahead", async () => {
|
|
224
|
+
installAdapter([
|
|
225
|
+
get(CALENDAR, () => [
|
|
226
|
+
call({ id: "c-past", title: "Last week's call", startsAt: at(8, 18), endsAt: at(8, 19) }),
|
|
227
|
+
call(),
|
|
228
|
+
event(),
|
|
229
|
+
]),
|
|
230
|
+
]);
|
|
231
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
232
|
+
await screen.findByTestId("community-calendar");
|
|
233
|
+
const days = await screen.findAllByTestId("community-calendar-day");
|
|
234
|
+
expect(days.map((day) => day.getAttribute("data-day"))).toEqual(["2026-09-16", "2026-09-18", "2026-09-08"]);
|
|
235
|
+
expect(screen.getByText("Already happened")).toBeTruthy();
|
|
236
|
+
expect(screen.getByTestId("community-calendar-month").textContent).toContain("September 2026");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("sends going and shows the new counts", async () => {
|
|
240
|
+
installAdapter([get(CALENDAR, () => [call()]), put(RSVP, () => ({ going: 3, notGoing: 1, mine: "going" }))]);
|
|
241
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
242
|
+
const going = await screen.findByTestId("community-calendar-rsvp-going");
|
|
243
|
+
expect(going.textContent).toBe("Going (2)");
|
|
244
|
+
expect(going.getAttribute("aria-pressed")).toBe("false");
|
|
245
|
+
fireEvent.click(going);
|
|
246
|
+
await waitFor(() => expect(screen.getByTestId("community-calendar-rsvp-going").textContent).toBe("Going (3)"));
|
|
247
|
+
expect(screen.getByTestId("community-calendar-rsvp-going").getAttribute("aria-pressed")).toBe("true");
|
|
248
|
+
expect(calls("put", RSVP)[0]!.body).toEqual({ profileId: PROFILE_ID, response: "going" });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("says a closed call takes no more answers on a 409", async () => {
|
|
252
|
+
installAdapter([
|
|
253
|
+
get(CALENDAR, () => [call()]),
|
|
254
|
+
put(RSVP, () => {
|
|
255
|
+
throw { status: 409, message: "Closed" };
|
|
256
|
+
}),
|
|
257
|
+
]);
|
|
258
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
259
|
+
fireEvent.click(await screen.findByTestId("community-calendar-rsvp-not_going"));
|
|
260
|
+
expect((await screen.findByTestId("community-calendar-rsvp-error")).textContent).toBe(
|
|
261
|
+
"This call has ended or was cancelled, so we're no longer taking answers for it.",
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("offers Join only while the room is open", async () => {
|
|
266
|
+
installAdapter([
|
|
267
|
+
get(CALENDAR, () => [
|
|
268
|
+
call({ id: "open", title: "Open now", isOpen: true, callPublicId: "pub-open", startsAt: at(15, 11), endsAt: at(15, 13) }),
|
|
269
|
+
call({ id: "later", title: "Later" }),
|
|
270
|
+
]),
|
|
271
|
+
]);
|
|
272
|
+
const went: string[] = [];
|
|
273
|
+
mount(<CommunityCalendar now={NOW} navigate={(href) => went.push(href)} />);
|
|
274
|
+
const rows = await screen.findAllByTestId("community-calendar-call");
|
|
275
|
+
expect(rows).toHaveLength(2);
|
|
276
|
+
const join = within(rows[0]!).getByTestId("community-calendar-join");
|
|
277
|
+
expect(within(rows[1]!).queryByTestId("community-calendar-join")).toBeNull();
|
|
278
|
+
expect(join.getAttribute("href")).toBe("/i/video-calls/pub-open");
|
|
279
|
+
fireEvent.click(join);
|
|
280
|
+
expect(went).toEqual(["/i/video-calls/pub-open"]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("opens a call's details from its title or Details, with when it opens, and Join once it is open", async () => {
|
|
284
|
+
installAdapter([
|
|
285
|
+
get(CALENDAR, () => [
|
|
286
|
+
call({ id: "later", title: "Later" }),
|
|
287
|
+
call({ id: "open", title: "Open now", isOpen: true, callPublicId: "pub-open", startsAt: at(15, 11), endsAt: at(15, 13) }),
|
|
288
|
+
]),
|
|
289
|
+
]);
|
|
290
|
+
const went: string[] = [];
|
|
291
|
+
mount(<CommunityCalendar now={NOW} navigate={(href) => went.push(href)} />);
|
|
292
|
+
const rows = await screen.findAllByTestId("community-calendar-call");
|
|
293
|
+
const later = rows.find((row) => row.textContent?.includes("Later"))!;
|
|
294
|
+
const open = rows.find((row) => row.textContent?.includes("Open now"))!;
|
|
295
|
+
|
|
296
|
+
fireEvent.click(within(later).getByTestId("community-calendar-call-open"));
|
|
297
|
+
const dialog = await screen.findByRole("dialog", { name: "Later" });
|
|
298
|
+
expect(dialog.textContent).toContain("Bring questions.");
|
|
299
|
+
expect(dialog.textContent).toContain("Fan club");
|
|
300
|
+
expect(within(dialog).queryByText("Join call")).toBeNull();
|
|
301
|
+
expect(dialog.textContent).toMatch(/Opens/i);
|
|
302
|
+
fireEvent.click(within(dialog).getByLabelText("Close"));
|
|
303
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
304
|
+
|
|
305
|
+
fireEvent.click(within(open).getByTestId("community-calendar-call-details"));
|
|
306
|
+
const openDialog = await screen.findByRole("dialog", { name: "Open now" });
|
|
307
|
+
fireEvent.click(within(openDialog).getByText("Join call"));
|
|
308
|
+
expect(went).toEqual(["/i/video-calls/pub-open"]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("gives every event a visible View event link, cancelled or past included", async () => {
|
|
312
|
+
installAdapter([
|
|
313
|
+
get(CALENDAR, () => [
|
|
314
|
+
event(),
|
|
315
|
+
event({ id: "event-2", slug: "called-off", title: "Called off", cancelled: true }),
|
|
316
|
+
event({ id: "event-3", slug: "last-week", title: "Last week", startsAt: at(8, 20), endsAt: at(8, 23) }),
|
|
317
|
+
]),
|
|
318
|
+
]);
|
|
319
|
+
const went: string[] = [];
|
|
320
|
+
mount(<CommunityCalendar now={NOW} navigate={(href) => went.push(href)} />);
|
|
321
|
+
const views = await screen.findAllByTestId("community-calendar-event-view");
|
|
322
|
+
expect(views.map((view) => view.getAttribute("href"))).toEqual([
|
|
323
|
+
"/i/events/album-launch",
|
|
324
|
+
"/i/events/called-off",
|
|
325
|
+
"/i/events/last-week",
|
|
326
|
+
]);
|
|
327
|
+
expect(views[0]!.textContent).toContain("View event");
|
|
328
|
+
fireEvent.click(views[1]!);
|
|
329
|
+
expect(went).toEqual(["/i/events/called-off"]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("links each upcoming item to its calendar file", async () => {
|
|
333
|
+
installAdapter([get(CALENDAR, () => [call(), event()])]);
|
|
334
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
335
|
+
const links = await screen.findAllByTestId("community-calendar-ics");
|
|
336
|
+
expect(links.map((link) => link.getAttribute("href"))).toEqual([
|
|
337
|
+
"https://api.test/public/calendar/calls/c1.ics",
|
|
338
|
+
"https://api.test/public/calendar/events/e1.ics",
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("links an event to its page, and marks a cancelled one", async () => {
|
|
343
|
+
installAdapter([
|
|
344
|
+
get(CALENDAR, () => [event(), event({ id: "event-2", slug: "called-off", title: "Called off", cancelled: true })]),
|
|
345
|
+
]);
|
|
346
|
+
const went: string[] = [];
|
|
347
|
+
mount(<CommunityCalendar now={NOW} navigate={(href) => went.push(href)} />);
|
|
348
|
+
const rows = await screen.findAllByTestId("community-calendar-event");
|
|
349
|
+
const link = within(rows[0]!).getByTestId("community-calendar-event-link");
|
|
350
|
+
expect(link.getAttribute("href")).toBe("/i/events/album-launch");
|
|
351
|
+
fireEvent.click(link);
|
|
352
|
+
expect(went).toEqual(["/i/events/album-launch"]);
|
|
353
|
+
expect(within(rows[0]!).queryByTestId("community-calendar-cancelled")).toBeNull();
|
|
354
|
+
expect(within(rows[1]!).getByTestId("community-calendar-cancelled").textContent).toBe("Cancelled");
|
|
355
|
+
expect(within(rows[1]!).queryByTestId("community-calendar-ics")).toBeNull();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("offers a past call's recording to rewatch, with its summary, and no answers any more", async () => {
|
|
359
|
+
installAdapter([
|
|
360
|
+
get(CALENDAR, () => [
|
|
361
|
+
call({
|
|
362
|
+
id: "c-past",
|
|
363
|
+
title: "Last week's call",
|
|
364
|
+
startsAt: at(8, 18),
|
|
365
|
+
endsAt: at(8, 19),
|
|
366
|
+
rsvp: { going: 4, notGoing: 0, mine: "going" },
|
|
367
|
+
recording: {
|
|
368
|
+
id: "r1",
|
|
369
|
+
status: "ready",
|
|
370
|
+
durationSeconds: 1800,
|
|
371
|
+
watchUrl: "https://cdn.test/rec.mp4?sig=abc",
|
|
372
|
+
transcript: {
|
|
373
|
+
status: "ready",
|
|
374
|
+
summary: { title: "Launch", summary: "They agreed to ship.", decisions: [], actionItems: [] },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
}),
|
|
378
|
+
]),
|
|
379
|
+
get("/public/scheduled-calls/mine/recordings/r1/transcript", () => ({
|
|
380
|
+
recordingId: "r1",
|
|
381
|
+
callId: "c-past",
|
|
382
|
+
title: "Last week's call",
|
|
383
|
+
startsAt: at(8, 18),
|
|
384
|
+
timezone: "UTC",
|
|
385
|
+
durationSeconds: 1800,
|
|
386
|
+
watchUrl: "https://cdn.test/rec.mp4?sig=fresh",
|
|
387
|
+
transcript: {
|
|
388
|
+
status: "ready",
|
|
389
|
+
language: "en",
|
|
390
|
+
segments: [{ name: "Ada", startMs: 0, endMs: 2_000, text: "Shall we ship Friday?" }],
|
|
391
|
+
summary: { title: "Launch", summary: "They agreed to ship.", decisions: [], actionItems: [] },
|
|
392
|
+
},
|
|
393
|
+
})),
|
|
394
|
+
]);
|
|
395
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
396
|
+
const row = await screen.findByTestId("community-calendar-call");
|
|
397
|
+
expect(screen.getByText("Already happened")).toBeTruthy();
|
|
398
|
+
const watch = within(row).getByTestId("call-recording-watch");
|
|
399
|
+
expect(watch.getAttribute("href")).toBe("https://cdn.test/rec.mp4?sig=abc");
|
|
400
|
+
expect(watch.textContent).toContain("Watch recording · 30 min");
|
|
401
|
+
expect(within(row).getByTestId("member-call-transcript").textContent).toContain("They agreed to ship.");
|
|
402
|
+
expect(within(row).queryByTestId("community-calendar-rsvp-going")).toBeNull();
|
|
403
|
+
expect(within(row).queryByTestId("community-calendar-ics")).toBeNull();
|
|
404
|
+
expect(within(row).queryByTestId("community-calendar-join")).toBeNull();
|
|
405
|
+
|
|
406
|
+
fireEvent.click(within(row).getByText("Read transcript"));
|
|
407
|
+
expect(await screen.findByTestId("member-transcript-video")).toBeTruthy();
|
|
408
|
+
expect(calls("get", "/public/scheduled-calls/mine/recordings/r1/transcript")).toHaveLength(1);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("says a past call's recording is still processing", async () => {
|
|
412
|
+
installAdapter([
|
|
413
|
+
get(CALENDAR, () => [
|
|
414
|
+
call({
|
|
415
|
+
startsAt: at(15, 9),
|
|
416
|
+
endsAt: at(15, 10),
|
|
417
|
+
recording: { id: "r2", status: "processing", durationSeconds: null, watchUrl: null, transcript: null },
|
|
418
|
+
}),
|
|
419
|
+
]),
|
|
420
|
+
]);
|
|
421
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
422
|
+
const row = await screen.findByTestId("community-calendar-call");
|
|
423
|
+
expect(within(row).getByTestId("call-recording-processing").textContent).toBe("Recording is processing");
|
|
424
|
+
expect(within(row).queryByTestId("call-recording-watch")).toBeNull();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("offers nothing to rewatch for a past call with no recording", async () => {
|
|
428
|
+
installAdapter([get(CALENDAR, () => [call({ startsAt: at(8, 18), endsAt: at(8, 19) })])]);
|
|
429
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
430
|
+
const row = await screen.findByTestId("community-calendar-call");
|
|
431
|
+
expect(within(row).queryByTestId("call-recording-watch")).toBeNull();
|
|
432
|
+
expect(within(row).queryByTestId("call-recording-processing")).toBeNull();
|
|
433
|
+
expect(within(row).queryByTestId("community-calendar-rsvp-going")).toBeNull();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("says so when the month is empty", async () => {
|
|
437
|
+
installAdapter([get(CALENDAR, () => [])]);
|
|
438
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
439
|
+
expect(await screen.findByTestId("community-calendar-empty")).toBeTruthy();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("tells a non-member the calendar is for members", async () => {
|
|
443
|
+
installAdapter([
|
|
444
|
+
get(CALENDAR, () => {
|
|
445
|
+
throw { status: 403, message: "Forbidden" };
|
|
446
|
+
}),
|
|
447
|
+
]);
|
|
448
|
+
mount(<CommunityCalendar now={NOW} />);
|
|
449
|
+
expect((await screen.findByTestId("community-calendar-forbidden")).textContent).toBe(
|
|
450
|
+
"The calendar is for members of this community.",
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
});
|