@tribe-nest/forge 3.60.0 → 3.61.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 +2 -1
- 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 +17 -0
- package/src/runtime/pages.spec.ts +5 -0
- package/src/runtime/pages.ts +32 -6
- package/src/types/models.ts +11 -0
- package/src/ui/analytics/ForgeAnalytics.tsx +64 -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/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/metaEvents.spec.ts +264 -0
- package/src/utils/landing.ts +19 -6
- package/src/utils/metaEvents.ts +302 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
|
4
|
+
import { ForgeThemeProvider } from "../../../theme/ForgeThemeProvider";
|
|
5
|
+
import { CommunityUpgradeOpenerProvider } from "../../../../contexts/CommunityContext";
|
|
6
|
+
import { CommunityHome } from "../CommunityHome";
|
|
7
|
+
import { CommunityMembershipMenu } from "../CommunityMembershipMenu";
|
|
8
|
+
import { CommunityJoinSheet } from "../CommunityJoinSheet";
|
|
9
|
+
import {
|
|
10
|
+
COMMUNITY,
|
|
11
|
+
account,
|
|
12
|
+
calls,
|
|
13
|
+
detailRoute,
|
|
14
|
+
get,
|
|
15
|
+
installAdapter,
|
|
16
|
+
meRoute,
|
|
17
|
+
post,
|
|
18
|
+
restoreAdapter,
|
|
19
|
+
tier,
|
|
20
|
+
viewer,
|
|
21
|
+
wrapper,
|
|
22
|
+
type Route,
|
|
23
|
+
} from "../../../headless/_tests/communityHarness";
|
|
24
|
+
import type { CommunityViewerState } from "../../../../data/queries/useCommunities";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The member area on a phone: a slim header whose tier pill opens the
|
|
28
|
+
* membership sheet, a bottom bar with "More", every sheet a bottom sheet below
|
|
29
|
+
* `lg`, the membership sheet's summary card and action rows, and the join
|
|
30
|
+
* sheet's email-first identity step.
|
|
31
|
+
*
|
|
32
|
+
* jsdom applies no CSS, so the responsive split is asserted through the
|
|
33
|
+
* classes that carry it (both headers render; which one shows is the class).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
afterEach(restoreAdapter);
|
|
37
|
+
|
|
38
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
39
|
+
|
|
40
|
+
function mount(node: React.ReactNode, opts: Parameters<typeof wrapper>[0] = {}) {
|
|
41
|
+
const Wrapper = wrapper(opts);
|
|
42
|
+
return render(
|
|
43
|
+
<Wrapper>
|
|
44
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
45
|
+
</Wrapper>,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const free = tier({ id: "tier-free", name: "Free" });
|
|
50
|
+
const gold = tier({ id: "tier-gold", name: "Gold", priceMonthly: 20 });
|
|
51
|
+
const basic = tier({ id: "tier-basic", name: "Basic", priceMonthly: 5 });
|
|
52
|
+
|
|
53
|
+
const member = (tierId: string, tierName: string, patch: Partial<NonNullable<CommunityViewerState["membership"]>> = {}, upgrade = false) =>
|
|
54
|
+
viewer({
|
|
55
|
+
state: "member",
|
|
56
|
+
membership: {
|
|
57
|
+
id: "m-1",
|
|
58
|
+
tierId,
|
|
59
|
+
tierName,
|
|
60
|
+
status: "active",
|
|
61
|
+
cancelAtPeriodEnd: false,
|
|
62
|
+
currentPeriodEnd: null,
|
|
63
|
+
pendingDowngradeTierId: null,
|
|
64
|
+
...patch,
|
|
65
|
+
},
|
|
66
|
+
upgradeTiers: upgrade ? [{ id: "tier-gold", name: "Gold", priceMonthly: 20, priceYearly: null }] : [],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** `/public/memberships/mine` for the member's one row, as the server envelopes it. */
|
|
70
|
+
const mineRoute = (membership: Record<string, unknown>): Route =>
|
|
71
|
+
get("/public/memberships/mine", () => [
|
|
72
|
+
{ community: { id: COMMUNITY.id, name: COMMUNITY.name, slug: COMMUNITY.slug }, membership: { id: "m-1", ...membership } },
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/** A community with spaces, chat and moderation, so the bar and "More" both have something. */
|
|
76
|
+
const busyCommunity: Route[] = [
|
|
77
|
+
get("/public/community/spaces", () => [{ id: "s-1", name: "General", slug: "general" }]),
|
|
78
|
+
get("/public/chat/channels", () => [{ id: "c-1", name: "general" }]),
|
|
79
|
+
get("/public/community/me", () => ({ canModerate: true })),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
describe("CommunityHome on a phone", () => {
|
|
83
|
+
it("one slim row: name, tier pill that opens the membership sheet, and Upgrade only when a higher tier exists", async () => {
|
|
84
|
+
installAdapter([meRoute(() => member("tier-free", "Free", {}, true)), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free, gold] }))]);
|
|
85
|
+
mount(
|
|
86
|
+
<CommunityHome tab="posts">
|
|
87
|
+
<p>feed</p>
|
|
88
|
+
</CommunityHome>,
|
|
89
|
+
{ user: account() },
|
|
90
|
+
);
|
|
91
|
+
const compact = await screen.findByTestId("community-home-header-compact");
|
|
92
|
+
expect(compact.className).toContain("fg:lg:hidden");
|
|
93
|
+
// The desktop header is untouched, and shows from lg up only.
|
|
94
|
+
expect(screen.getByTestId("community-home-header").className).toContain("fg:lg:flex");
|
|
95
|
+
expect(within(compact).getByRole("heading", { name: "Fan Club" })).toBeTruthy();
|
|
96
|
+
expect(await within(compact).findByTestId("community-home-upgrade-chip")).toBeTruthy();
|
|
97
|
+
|
|
98
|
+
fireEvent.click(within(compact).getByTestId("community-home-tier-button"));
|
|
99
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
100
|
+
expect(within(sheet).getByTestId("community-membership-summary").textContent).toContain("Free");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("no Upgrade chip when there is nowhere higher to go", async () => {
|
|
104
|
+
installAdapter([meRoute(() => member("tier-gold", "Gold")), mineRoute({ amount: 20, currency: "USD", billingCycle: "month" }), detailRoute(() => ({ tiers: [free, gold] }))]);
|
|
105
|
+
mount(
|
|
106
|
+
<CommunityHome tab="posts">
|
|
107
|
+
<p>feed</p>
|
|
108
|
+
</CommunityHome>,
|
|
109
|
+
{ user: account() },
|
|
110
|
+
);
|
|
111
|
+
await screen.findByTestId("community-home-tier-button");
|
|
112
|
+
expect(screen.queryByTestId("community-home-upgrade-chip")).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("the bottom bar: Feed, Spaces, Chat, Alerts and More; More lists the rest, grouped, and Your membership", async () => {
|
|
116
|
+
const went: string[] = [];
|
|
117
|
+
installAdapter([...busyCommunity, meRoute(() => member("tier-free", "Free")), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free] }))]);
|
|
118
|
+
mount(
|
|
119
|
+
<CommunityHome tab="posts" navigate={(href) => went.push(href)}>
|
|
120
|
+
<p>feed</p>
|
|
121
|
+
</CommunityHome>,
|
|
122
|
+
{ user: account() },
|
|
123
|
+
);
|
|
124
|
+
const bar = await screen.findByTestId("community-bar");
|
|
125
|
+
expect(bar.className).toContain("fg:lg:hidden");
|
|
126
|
+
await waitFor(() =>
|
|
127
|
+
expect(within(bar).getAllByRole("link").map((a) => a.textContent)).toEqual(["Feed", "Spaces", "Chat", "Alerts"]),
|
|
128
|
+
);
|
|
129
|
+
expect(within(bar).getByTestId("community-bar-posts").getAttribute("aria-current")).toBe("page");
|
|
130
|
+
expect(within(bar).getByTestId("community-bar-more").getAttribute("aria-pressed")).toBe("false");
|
|
131
|
+
// The page ends above the bar.
|
|
132
|
+
expect(screen.getByRole("main").className).toContain("fg:pb-[calc(72px+env(safe-area-inset-bottom))]");
|
|
133
|
+
|
|
134
|
+
fireEvent.click(within(bar).getByTestId("community-bar-chat"));
|
|
135
|
+
expect(went).toEqual(["/i/communities/fan-club/home/chat"]);
|
|
136
|
+
|
|
137
|
+
fireEvent.click(within(bar).getByTestId("community-bar-more"));
|
|
138
|
+
const more = await screen.findByTestId("community-more-sheet");
|
|
139
|
+
expect(within(more).getByText("Community")).toBeTruthy();
|
|
140
|
+
expect(within(more).getByText("You")).toBeTruthy();
|
|
141
|
+
const rows = within(more)
|
|
142
|
+
.getAllByRole("button")
|
|
143
|
+
.map((b) => b.getAttribute("data-testid"))
|
|
144
|
+
.filter((id) => id?.startsWith("community-more-") && id !== "community-more-sheet");
|
|
145
|
+
expect(rows).toEqual([
|
|
146
|
+
"community-more-calendar",
|
|
147
|
+
"community-more-classroom",
|
|
148
|
+
"community-more-members",
|
|
149
|
+
"community-more-leaderboard",
|
|
150
|
+
"community-more-membership",
|
|
151
|
+
"community-more-moderation",
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
fireEvent.click(within(more).getByTestId("community-more-membership"));
|
|
155
|
+
expect(await screen.findByTestId("community-membership-menu")).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("a page that lives under More highlights More", async () => {
|
|
159
|
+
installAdapter([...busyCommunity, meRoute(() => member("tier-free", "Free")), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free] }))]);
|
|
160
|
+
mount(
|
|
161
|
+
<CommunityHome tab="members">
|
|
162
|
+
<p>members</p>
|
|
163
|
+
</CommunityHome>,
|
|
164
|
+
{ user: account() },
|
|
165
|
+
);
|
|
166
|
+
const more = await screen.findByTestId("community-bar-more");
|
|
167
|
+
expect(more.getAttribute("aria-pressed")).toBe("true");
|
|
168
|
+
expect(screen.getByTestId("community-bar-posts").getAttribute("aria-current")).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("publishes the bar's height for the chat bubble on a phone, and clears it on the way out", async () => {
|
|
172
|
+
const original = window.matchMedia;
|
|
173
|
+
window.matchMedia = ((query: string) => ({
|
|
174
|
+
matches: true,
|
|
175
|
+
media: query,
|
|
176
|
+
addEventListener: () => {},
|
|
177
|
+
removeEventListener: () => {},
|
|
178
|
+
})) as unknown as typeof window.matchMedia;
|
|
179
|
+
try {
|
|
180
|
+
installAdapter([...busyCommunity, meRoute(() => member("tier-free", "Free")), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free] }))]);
|
|
181
|
+
const view = mount(
|
|
182
|
+
<CommunityHome tab="posts">
|
|
183
|
+
<p>feed</p>
|
|
184
|
+
</CommunityHome>,
|
|
185
|
+
{ user: account() },
|
|
186
|
+
);
|
|
187
|
+
await screen.findByTestId("community-bar");
|
|
188
|
+
await waitFor(() => expect(document.documentElement.style.getPropertyValue("--forge-bottom-bar-height")).toBe("56px"));
|
|
189
|
+
view.unmount();
|
|
190
|
+
expect(document.documentElement.style.getPropertyValue("--forge-bottom-bar-height")).toBe("");
|
|
191
|
+
} finally {
|
|
192
|
+
window.matchMedia = original;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("lifts the chat bubble clear of the message box in chat only, and clears it on the way out", async () => {
|
|
197
|
+
const original = window.matchMedia;
|
|
198
|
+
window.matchMedia = ((query: string) => ({
|
|
199
|
+
matches: true,
|
|
200
|
+
media: query,
|
|
201
|
+
addEventListener: () => {},
|
|
202
|
+
removeEventListener: () => {},
|
|
203
|
+
})) as unknown as typeof window.matchMedia;
|
|
204
|
+
const lift = () => document.documentElement.style.getPropertyValue("--forge-floating-lift");
|
|
205
|
+
try {
|
|
206
|
+
installAdapter([...busyCommunity, meRoute(() => member("tier-free", "Free")), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free] }))]);
|
|
207
|
+
const feed = mount(
|
|
208
|
+
<CommunityHome tab="posts">
|
|
209
|
+
<p>feed</p>
|
|
210
|
+
</CommunityHome>,
|
|
211
|
+
{ user: account() },
|
|
212
|
+
);
|
|
213
|
+
await screen.findByTestId("community-bar");
|
|
214
|
+
expect(lift()).toBe("");
|
|
215
|
+
feed.unmount();
|
|
216
|
+
|
|
217
|
+
const chat = mount(
|
|
218
|
+
<CommunityHome tab="chat">
|
|
219
|
+
<p>chat</p>
|
|
220
|
+
</CommunityHome>,
|
|
221
|
+
{ user: account() },
|
|
222
|
+
);
|
|
223
|
+
await screen.findByTestId("community-bar");
|
|
224
|
+
await waitFor(() => expect(lift()).toBe("88px"));
|
|
225
|
+
chat.unmount();
|
|
226
|
+
expect(lift()).toBe("");
|
|
227
|
+
} finally {
|
|
228
|
+
window.matchMedia = original;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("CommunitySheet below lg", () => {
|
|
234
|
+
it("is a bottom sheet with a grab handle above the chat bubble, and the centred dialog from lg up", async () => {
|
|
235
|
+
installAdapter([meRoute(() => member("tier-free", "Free")), mineRoute({ amount: null }), detailRoute(() => ({ tiers: [free] }))]);
|
|
236
|
+
mount(<CommunityMembershipMenu open onOpenChange={() => {}} />, { user: account() });
|
|
237
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
238
|
+
const cls = sheet.className;
|
|
239
|
+
for (const c of ["fg:fixed", "fg:inset-x-0", "fg:bottom-0", "fg:w-full", "fg:max-h-[90dvh]", "fg:overflow-y-auto", "fg:rounded-t-[var(--cs-radius)]"]) {
|
|
240
|
+
expect(cls).toContain(c);
|
|
241
|
+
}
|
|
242
|
+
for (const c of ["fg:lg:top-1/2", "fg:lg:left-1/2", "fg:lg:-translate-x-1/2", "fg:lg:-translate-y-1/2", "fg:lg:w-[min(560px,calc(100vw-32px))]"]) {
|
|
243
|
+
expect(cls).toContain(c);
|
|
244
|
+
}
|
|
245
|
+
expect(sheet.style.zIndex).toBe("10001");
|
|
246
|
+
expect(sheet.style.paddingBottom).toContain("safe-area-inset-bottom");
|
|
247
|
+
const handle = sheet.querySelector("[data-sheet-handle]") as HTMLElement;
|
|
248
|
+
expect(handle.className).toContain("fg:lg:hidden");
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("the membership sheet", () => {
|
|
253
|
+
const fmt = (amount: number, currency?: string) => `${currency} ${amount}`;
|
|
254
|
+
|
|
255
|
+
it("free: says Free, offers no payment method, and the way out is Leave community, which still asks first", async () => {
|
|
256
|
+
installAdapter([
|
|
257
|
+
post("/public/communities/fan-club/membership/cancel", () => ({})),
|
|
258
|
+
meRoute(() => member("tier-free", "Free", {}, true)),
|
|
259
|
+
mineRoute({ amount: null, currency: null, billingCycle: null }),
|
|
260
|
+
detailRoute(() => ({ tiers: [free, gold] })),
|
|
261
|
+
]);
|
|
262
|
+
mount(
|
|
263
|
+
<CommunityUpgradeOpenerProvider value={{ openUpgrade: () => {} }}>
|
|
264
|
+
<CommunityMembershipMenu open onOpenChange={() => {}} formatAmount={fmt} />
|
|
265
|
+
</CommunityUpgradeOpenerProvider>,
|
|
266
|
+
{ user: account() },
|
|
267
|
+
);
|
|
268
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
269
|
+
await waitFor(() => expect(calls("get", "/public/memberships/mine")).toHaveLength(1));
|
|
270
|
+
expect(within(sheet).getByTestId("community-membership-price").textContent).toBe("Free");
|
|
271
|
+
expect(within(sheet).getByTestId("community-membership-upgrade")).toBeTruthy();
|
|
272
|
+
expect(within(sheet).queryByTestId("community-membership-row-update-card")).toBeNull();
|
|
273
|
+
const leave = within(sheet).getByTestId("community-membership-row-cancel");
|
|
274
|
+
expect(leave.textContent).toBe("Leave community");
|
|
275
|
+
|
|
276
|
+
fireEvent.click(leave);
|
|
277
|
+
const confirm = within(sheet).getByTestId("community-membership-cancel-confirm");
|
|
278
|
+
expect(confirm.textContent).toContain("Leave this community?");
|
|
279
|
+
expect(confirm.textContent).toContain("You lose access now.");
|
|
280
|
+
expect(calls("post", "/public/communities/fan-club/membership/cancel")).toHaveLength(0);
|
|
281
|
+
fireEvent.click(within(confirm).getByTestId("community-membership-cancel-confirm-button"));
|
|
282
|
+
await waitFor(() => expect(calls("post", "/public/communities/fan-club/membership/cancel")).toHaveLength(1));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("paid: the price per period, the renewal date, update card, switch down, and Cancel membership with its confirmation", async () => {
|
|
286
|
+
installAdapter([
|
|
287
|
+
meRoute(() => member("tier-gold", "Gold", { currentPeriodEnd: "2026-10-27T00:00:00.000Z" })),
|
|
288
|
+
mineRoute({ amount: 20, currency: "USD", billingCycle: "month" }),
|
|
289
|
+
detailRoute(() => ({ tiers: [free, basic, gold] })),
|
|
290
|
+
]);
|
|
291
|
+
mount(
|
|
292
|
+
<CommunityUpgradeOpenerProvider value={{ openUpgrade: () => {} }}>
|
|
293
|
+
<CommunityMembershipMenu open onOpenChange={() => {}} formatAmount={fmt} />
|
|
294
|
+
</CommunityUpgradeOpenerProvider>,
|
|
295
|
+
{ user: account() },
|
|
296
|
+
);
|
|
297
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
298
|
+
await waitFor(() => expect(within(sheet).getByTestId("community-membership-price").textContent).toBe("USD 20 per month"));
|
|
299
|
+
expect(within(sheet).getByTestId("community-membership-period").textContent).toMatch(/^Renews on .*2026/);
|
|
300
|
+
// No higher tier: no Upgrade.
|
|
301
|
+
expect(within(sheet).queryByTestId("community-membership-upgrade")).toBeNull();
|
|
302
|
+
expect(within(sheet).getByTestId("community-membership-row-update-card").textContent).toBe("Update payment method");
|
|
303
|
+
await waitFor(() =>
|
|
304
|
+
expect(within(sheet).getByTestId("community-membership-row-downgrade-tier-basic").textContent).toBe("Switch to Basic at renewal"),
|
|
305
|
+
);
|
|
306
|
+
expect(within(sheet).getByTestId("community-membership-row-downgrade-tier-free")).toBeTruthy();
|
|
307
|
+
|
|
308
|
+
const cancel = within(sheet).getByTestId("community-membership-row-cancel");
|
|
309
|
+
expect(cancel.textContent).toBe("Cancel membership");
|
|
310
|
+
fireEvent.click(cancel);
|
|
311
|
+
const confirm = within(sheet).getByTestId("community-membership-cancel-confirm");
|
|
312
|
+
expect(confirm.getAttribute("role")).toBe("alertdialog");
|
|
313
|
+
expect(confirm.textContent).toContain("Cancel your membership?");
|
|
314
|
+
expect(confirm.textContent).toMatch(/You keep access until .*2026/);
|
|
315
|
+
// The row is replaced by the confirmation while it is open.
|
|
316
|
+
expect(within(sheet).queryByTestId("community-membership-row-cancel")).toBeNull();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("a yearly subscription reads per year; an ending one says Ending and Ends on, with no way to cancel twice", async () => {
|
|
320
|
+
installAdapter([
|
|
321
|
+
meRoute(() => member("tier-gold", "Gold", { currentPeriodEnd: "2027-01-01T00:00:00.000Z", cancelAtPeriodEnd: true })),
|
|
322
|
+
mineRoute({ amount: 200, currency: "EUR", billingCycle: "year", cancelAtPeriodEnd: true }),
|
|
323
|
+
detailRoute(() => ({ tiers: [free, gold] })),
|
|
324
|
+
]);
|
|
325
|
+
mount(<CommunityMembershipMenu open onOpenChange={() => {}} formatAmount={fmt} />, { user: account() });
|
|
326
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
327
|
+
await waitFor(() => expect(within(sheet).getByTestId("community-membership-price").textContent).toBe("EUR 200 per year"));
|
|
328
|
+
expect(within(sheet).getByTestId("community-membership-summary").textContent).toContain("Ending");
|
|
329
|
+
expect(within(sheet).getByTestId("community-membership-period").textContent).toMatch(/^Ends on /);
|
|
330
|
+
expect(within(sheet).queryByTestId("community-membership-row-cancel")).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("before the price arrives, a paid tier still hides nothing it needs and a free one never shows a card row", async () => {
|
|
334
|
+
installAdapter([
|
|
335
|
+
meRoute(() => member("tier-free", "Free")),
|
|
336
|
+
// `/mine` never answers with a row for this membership.
|
|
337
|
+
get("/public/memberships/mine", () => []),
|
|
338
|
+
detailRoute(() => ({ tiers: [free] })),
|
|
339
|
+
]);
|
|
340
|
+
mount(<CommunityMembershipMenu open onOpenChange={() => {}} />, { user: account() });
|
|
341
|
+
const sheet = await screen.findByTestId("community-membership-menu");
|
|
342
|
+
await waitFor(() => expect(within(sheet).getByTestId("community-membership-price").textContent).toBe("Free"));
|
|
343
|
+
expect(within(sheet).queryByTestId("community-membership-row-update-card")).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("the join sheet, email first", () => {
|
|
348
|
+
it("asks for an email only, with no Create account / Sign in pair, and a password link", async () => {
|
|
349
|
+
installAdapter([
|
|
350
|
+
post("/public/sessions/email/start", (req) => ({ status: "code_sent", email: req.body?.email })),
|
|
351
|
+
detailRoute(() => ({ tiers: [free], viewer: viewer({ state: "anonymous" }) })),
|
|
352
|
+
]);
|
|
353
|
+
mount(<CommunityJoinSheet open onOpenChange={() => {}} />);
|
|
354
|
+
const sheet = await screen.findByTestId("community-join-sheet");
|
|
355
|
+
expect(within(sheet).getByRole("heading", { name: "Join Fan Club" })).toBeTruthy();
|
|
356
|
+
const form = await within(sheet).findByTestId("community-join-email");
|
|
357
|
+
expect(within(sheet).queryByRole("tab")).toBeNull();
|
|
358
|
+
expect(form.querySelector('input[type="password"]')).toBeNull();
|
|
359
|
+
expect(within(form).getByTestId("community-join-use-password").textContent).toBe("Use your password instead");
|
|
360
|
+
|
|
361
|
+
fireEvent.change(within(form).getByLabelText("Email"), { target: { value: "fan@example.com" } });
|
|
362
|
+
fireEvent.submit(form);
|
|
363
|
+
await waitFor(() => expect(within(sheet).getByText("We emailed a 6-digit code to fan@example.com.")).toBeTruthy());
|
|
364
|
+
expect(within(sheet).getByTestId("community-join-change-email").textContent).toBe("Use a different email");
|
|
365
|
+
expect(within(sheet).getByTestId("community-join-resend").textContent).toBe("Resend code");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("the new-person step asks for a name and the terms, and only then", async () => {
|
|
369
|
+
installAdapter([
|
|
370
|
+
post("/public/sessions/email/start", (req) => ({ status: "code_sent", email: req.body?.email })),
|
|
371
|
+
post("/public/sessions/email/verify", () => ({ status: "needs_profile", ticket: "ab".repeat(32) })),
|
|
372
|
+
detailRoute(() => ({ tiers: [free], viewer: viewer({ state: "anonymous" }) })),
|
|
373
|
+
]);
|
|
374
|
+
mount(<CommunityJoinSheet open onOpenChange={() => {}} />);
|
|
375
|
+
const sheet = await screen.findByTestId("community-join-sheet");
|
|
376
|
+
const form = await within(sheet).findByTestId("community-join-email");
|
|
377
|
+
fireEvent.change(within(form).getByLabelText("Email"), { target: { value: "new@example.com" } });
|
|
378
|
+
fireEvent.submit(form);
|
|
379
|
+
const code = await within(sheet).findByLabelText("6-digit code");
|
|
380
|
+
fireEvent.change(code, { target: { value: "123456" } });
|
|
381
|
+
fireEvent.submit(code.closest("form")!);
|
|
382
|
+
const profile = await within(sheet).findByTestId("community-join-profile");
|
|
383
|
+
expect(within(profile).getByLabelText("First name")).toBeTruthy();
|
|
384
|
+
expect(within(profile).getByLabelText("Last name")).toBeTruthy();
|
|
385
|
+
expect(within(profile).getByLabelText("I accept the Terms and Privacy Policy")).toBeTruthy();
|
|
386
|
+
expect(within(profile).getByTestId("community-join-start-again")).toBeTruthy();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("Use your password instead shows the email and password sign-in, and a way back", async () => {
|
|
390
|
+
installAdapter([detailRoute(() => ({ tiers: [free], viewer: viewer({ state: "anonymous" }) }))]);
|
|
391
|
+
mount(<CommunityJoinSheet open onOpenChange={() => {}} />);
|
|
392
|
+
const sheet = await screen.findByTestId("community-join-sheet");
|
|
393
|
+
fireEvent.click(await within(sheet).findByTestId("community-join-use-password"));
|
|
394
|
+
expect(within(sheet).getByLabelText("Password")).toBeTruthy();
|
|
395
|
+
expect(within(sheet).queryByLabelText("First name")).toBeNull();
|
|
396
|
+
fireEvent.click(within(sheet).getByTestId("community-join-use-code"));
|
|
397
|
+
expect(await within(sheet).findByTestId("community-join-email")).toBeTruthy();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { CommunityWiki } from "../CommunityWiki";
|
|
8
|
+
import { CommunityPublicWiki } from "../CommunityPublicWiki";
|
|
9
|
+
import { CommunityRichText } from "../../community/CommunityRichText";
|
|
10
|
+
import {
|
|
11
|
+
account,
|
|
12
|
+
calls,
|
|
13
|
+
get,
|
|
14
|
+
installAdapter,
|
|
15
|
+
restoreAdapter,
|
|
16
|
+
wrapper,
|
|
17
|
+
} from "../../../headless/_tests/communityHarness";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The wiki's markup side: the list beside the open page, a locked page's tiers
|
|
21
|
+
* and upgrade (through the community's upgrade sheet), the table of contents
|
|
22
|
+
* linking to anchored headings, and the About page's public pages opening in
|
|
23
|
+
* place.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
cleanup();
|
|
28
|
+
restoreAdapter();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
32
|
+
|
|
33
|
+
function mount(node: ReactNode, opts: Parameters<typeof wrapper>[0] = { user: account() }) {
|
|
34
|
+
const Wrapper = wrapper(opts);
|
|
35
|
+
return render(
|
|
36
|
+
<Wrapper>
|
|
37
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
38
|
+
</Wrapper>,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const text = (value: string) => ({ type: "text", version: 1, text: value });
|
|
43
|
+
const doc = (...children: unknown[]) => ({ root: { type: "root", version: 1, children } });
|
|
44
|
+
|
|
45
|
+
const PAGES = [
|
|
46
|
+
{ id: "p1", slug: "start-here", title: "Start here", isPublic: false, locked: false, unlockTiers: [], updatedAt: "" },
|
|
47
|
+
{
|
|
48
|
+
id: "p2",
|
|
49
|
+
slug: "vip-lounge",
|
|
50
|
+
title: "VIP lounge",
|
|
51
|
+
isPublic: false,
|
|
52
|
+
locked: true,
|
|
53
|
+
unlockTiers: [
|
|
54
|
+
{ id: "tier-gold", name: "Gold" },
|
|
55
|
+
{ id: "tier-plat", name: "Platinum" },
|
|
56
|
+
],
|
|
57
|
+
updatedAt: "",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "p3",
|
|
61
|
+
slug: "community-guidelines",
|
|
62
|
+
title: "Community guidelines",
|
|
63
|
+
isPublic: true,
|
|
64
|
+
locked: false,
|
|
65
|
+
unlockTiers: [],
|
|
66
|
+
updatedAt: "",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const startHere = get("/public/communities/fan-club/wiki/start-here", () => ({
|
|
71
|
+
id: "p1",
|
|
72
|
+
title: "Start here",
|
|
73
|
+
slug: "start-here",
|
|
74
|
+
isPublic: false,
|
|
75
|
+
updatedAt: "",
|
|
76
|
+
content: doc(
|
|
77
|
+
{ type: "heading", version: 1, tag: "h2", children: [text("Welcome")] },
|
|
78
|
+
{ type: "paragraph", version: 1, children: [text("Glad you are here.")] },
|
|
79
|
+
{ type: "heading", version: 1, tag: "h2", children: [text("Next steps")] },
|
|
80
|
+
{
|
|
81
|
+
type: "list",
|
|
82
|
+
version: 1,
|
|
83
|
+
listType: "bullet",
|
|
84
|
+
children: [{ type: "listitem", version: 1, children: [text("Say hello")] }],
|
|
85
|
+
},
|
|
86
|
+
{ type: "horizontalrule", version: 1 },
|
|
87
|
+
{ type: "kb-image", version: 1, src: "https://cdn.example.com/a.png", altText: "A map" },
|
|
88
|
+
{ type: "kb-image", version: 1, src: "kb-private:abc", altText: "secret" },
|
|
89
|
+
),
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
describe("CommunityWiki (the tab)", () => {
|
|
93
|
+
it("opens the first readable page with a table of contents linking to its headings", async () => {
|
|
94
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES), startHere]);
|
|
95
|
+
const { container } = mount(<CommunityWiki getPageHref={(page) => `/wiki/${page}`} />);
|
|
96
|
+
const page = await screen.findByTestId("community-wiki-page");
|
|
97
|
+
expect(within(page).getByRole("heading", { level: 1 }).textContent).toBe("Start here");
|
|
98
|
+
|
|
99
|
+
const toc = screen.getByTestId("community-wiki-toc");
|
|
100
|
+
expect(
|
|
101
|
+
within(toc)
|
|
102
|
+
.getAllByRole("link")
|
|
103
|
+
.map((a) => a.getAttribute("href")),
|
|
104
|
+
).toEqual(["#welcome", "#next-steps"]);
|
|
105
|
+
expect(container.querySelector("#welcome")?.textContent).toBe("Welcome");
|
|
106
|
+
expect(container.querySelector("#next-steps")?.textContent).toBe("Next steps");
|
|
107
|
+
expect(within(page).getByText("Say hello")).toBeTruthy();
|
|
108
|
+
expect(page.querySelector("hr")).toBeTruthy();
|
|
109
|
+
// A public image renders; a private Knowledge Base reference has no address on a site.
|
|
110
|
+
expect(
|
|
111
|
+
within(page)
|
|
112
|
+
.getAllByRole("img")
|
|
113
|
+
.map((img) => img.getAttribute("alt")),
|
|
114
|
+
).toEqual(["A map"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("lists a locked page with its tiers, and upgrades through the community's sheet", async () => {
|
|
118
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES)]);
|
|
119
|
+
const opened: (string | null | undefined)[] = [];
|
|
120
|
+
mount(
|
|
121
|
+
<CommunityUpgradeOpenerProvider value={{ openUpgrade: (tierId) => opened.push(tierId) }}>
|
|
122
|
+
<CommunityWiki pageSlug="vip-lounge" getPageHref={(page) => `/wiki/${page}`} />
|
|
123
|
+
</CommunityUpgradeOpenerProvider>,
|
|
124
|
+
);
|
|
125
|
+
const locked = await screen.findByTestId("community-wiki-locked");
|
|
126
|
+
expect(locked.textContent).toContain("This page is part of Gold, Platinum.");
|
|
127
|
+
fireEvent.click(within(locked).getByTestId("community-wiki-upgrade"));
|
|
128
|
+
expect(opened).toEqual(["tier-gold"]);
|
|
129
|
+
// A locked page is never fetched: the server would only say not found.
|
|
130
|
+
expect(calls("get", "/public/communities/fan-club/wiki/vip-lounge")).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("navigates through the host's router from the list", async () => {
|
|
134
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => PAGES), startHere]);
|
|
135
|
+
const went: string[] = [];
|
|
136
|
+
mount(<CommunityWiki navigate={(href) => went.push(href)} getPageHref={(page) => `/wiki/${page}`} />);
|
|
137
|
+
const items = await screen.findAllByTestId("community-wiki-item");
|
|
138
|
+
fireEvent.click(within(items[2]).getByRole("link"));
|
|
139
|
+
expect(went).toEqual(["/wiki/community-guidelines"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("says so when a page is not available", async () => {
|
|
143
|
+
installAdapter([
|
|
144
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
145
|
+
get("/public/communities/fan-club/wiki/gone", () => {
|
|
146
|
+
throw { status: 404, message: "Not found" };
|
|
147
|
+
}),
|
|
148
|
+
]);
|
|
149
|
+
mount(<CommunityWiki pageSlug="gone" getPageHref={(page) => `/wiki/${page}`} />);
|
|
150
|
+
expect(await screen.findByText("This page isn't available to you.")).toBeTruthy();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("private wiki images", () => {
|
|
155
|
+
const privatePage = get("/public/communities/fan-club/wiki/start-here", () => ({
|
|
156
|
+
id: "p1",
|
|
157
|
+
title: "Start here",
|
|
158
|
+
slug: "start-here",
|
|
159
|
+
isPublic: false,
|
|
160
|
+
updatedAt: "",
|
|
161
|
+
content: doc(
|
|
162
|
+
{ type: "kb-image", version: 1, src: "kbmedia:m-1", altText: "Floor plan" },
|
|
163
|
+
{ type: "kb-image", version: 1, src: "kbmedia:m-2", altText: "Gone" },
|
|
164
|
+
),
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
it("shows a placeholder, then the signed image; a refused one is quietly unavailable", async () => {
|
|
168
|
+
installAdapter([
|
|
169
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
170
|
+
privatePage,
|
|
171
|
+
get("/public/communities/fan-club/wiki/start-here/media/m-1", () => ({
|
|
172
|
+
url: "https://signed.example.com/m-1",
|
|
173
|
+
expiresInSeconds: 600,
|
|
174
|
+
})),
|
|
175
|
+
get("/public/communities/fan-club/wiki/start-here/media/m-2", () => {
|
|
176
|
+
throw { status: 404, message: "Not found" };
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
179
|
+
mount(<CommunityWiki getPageHref={(page) => `/wiki/${page}`} />);
|
|
180
|
+
expect((await screen.findAllByTestId("community-wiki-image-placeholder")).length).toBeGreaterThan(0);
|
|
181
|
+
const img = await screen.findByTestId("community-wiki-private-image");
|
|
182
|
+
expect(img.getAttribute("src")).toBe("https://signed.example.com/m-1");
|
|
183
|
+
expect(img.getAttribute("alt")).toBe("Floor plan");
|
|
184
|
+
expect((await screen.findByTestId("community-wiki-image-unavailable")).textContent).toBe("Image unavailable");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("is never resolved outside the wiki: a post renders nothing for it", async () => {
|
|
188
|
+
installAdapter([]);
|
|
189
|
+
mount(<CommunityRichText body={doc({ type: "kb-image", version: 1, src: "kbmedia:m-1", altText: "x" })} />);
|
|
190
|
+
expect(screen.queryByRole("img")).toBeNull();
|
|
191
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
192
|
+
expect(calls("get", /\/media\//)).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("CommunityPublicWiki (on the About page)", () => {
|
|
197
|
+
it("lists only the public pages, and opens one in place", async () => {
|
|
198
|
+
installAdapter([
|
|
199
|
+
get("/public/communities/fan-club/wiki", () => PAGES),
|
|
200
|
+
get("/public/communities/fan-club/wiki/community-guidelines", () => ({
|
|
201
|
+
id: "p3",
|
|
202
|
+
title: "Community guidelines",
|
|
203
|
+
slug: "community-guidelines",
|
|
204
|
+
isPublic: true,
|
|
205
|
+
updatedAt: "",
|
|
206
|
+
content: doc({ type: "paragraph", version: 1, children: [text("Be kind.")] }),
|
|
207
|
+
})),
|
|
208
|
+
]);
|
|
209
|
+
mount(<CommunityPublicWiki />, {});
|
|
210
|
+
const items = await screen.findAllByTestId("community-public-wiki-item");
|
|
211
|
+
expect(items.map((item) => item.textContent)).toEqual(["Community guidelines"]);
|
|
212
|
+
expect(calls("get", "/public/communities/fan-club/wiki/community-guidelines")).toHaveLength(0);
|
|
213
|
+
fireEvent.click(within(items[0]).getByRole("button", { name: "Community guidelines" }));
|
|
214
|
+
await waitFor(() => expect(screen.getByText("Be kind.")).toBeTruthy());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("renders nothing when there are no public pages", async () => {
|
|
218
|
+
installAdapter([get("/public/communities/fan-club/wiki", () => [PAGES[0]])]);
|
|
219
|
+
mount(<CommunityPublicWiki />, {});
|
|
220
|
+
await waitFor(() => expect(calls("get", "/public/communities/fan-club/wiki")).toHaveLength(1));
|
|
221
|
+
expect(screen.queryByTestId("community-public-wiki")).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -8,3 +8,12 @@ export { CommunityHome, type CommunityHomeProps, type CommunityHomeTab } from ".
|
|
|
8
8
|
export { CommunityUpgradeSheet, type CommunityUpgradeSheetProps } from "./CommunityUpgradeSheet";
|
|
9
9
|
export { CommunityMembershipMenu, type CommunityMembershipMenuProps } from "./CommunityMembershipMenu";
|
|
10
10
|
export { MyMembershipsList, type MyMembershipsListProps } from "./MyMembershipsList";
|
|
11
|
+
export {
|
|
12
|
+
CommunityWiki,
|
|
13
|
+
CommunityWikiPageView,
|
|
14
|
+
type CommunityWikiProps,
|
|
15
|
+
type CommunityWikiPageViewProps,
|
|
16
|
+
} from "./CommunityWiki";
|
|
17
|
+
export { CommunityPublicWiki, type CommunityPublicWikiProps } from "./CommunityPublicWiki";
|
|
18
|
+
export { CommunityClassroom, type CommunityClassroomProps } from "./CommunityClassroom";
|
|
19
|
+
export { CommunityCalendar, type CommunityCalendarProps } from "./CommunityCalendar";
|