@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,386 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } 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 { CommunityComposer } from "../CommunityComposer";
|
|
7
|
+
import { CommunityPollCard } from "../CommunityPollCard";
|
|
8
|
+
import { CommunitySpaces } from "../CommunitySpaces";
|
|
9
|
+
import { CommunityNotificationsInbox } from "../CommunityNotifications";
|
|
10
|
+
import { CommunityWiki } from "../../communities/CommunityWiki";
|
|
11
|
+
import type { CommunityPollView } from "../../../../data/queries/useCommunity";
|
|
12
|
+
import {
|
|
13
|
+
COMMUNITY,
|
|
14
|
+
account,
|
|
15
|
+
calls,
|
|
16
|
+
get,
|
|
17
|
+
installAdapter,
|
|
18
|
+
post,
|
|
19
|
+
restoreAdapter,
|
|
20
|
+
wrapper,
|
|
21
|
+
} from "../../../headless/_tests/communityHarness";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Polls and level locks as a member meets them: the composer's Poll section
|
|
25
|
+
* and its visible errors, the poll card in each of its states (open, hidden
|
|
26
|
+
* results, results shown, closed), a space above the member's level listed
|
|
27
|
+
* but not a way in, a wiki page a level opens, and the level-up notice.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
cleanup();
|
|
32
|
+
restoreAdapter();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const theme = { colors: { text: "#111111", background: "#ffffff", primary: "#6d28d9" }, cornerRadius: 8 };
|
|
36
|
+
|
|
37
|
+
function mount(node: ReactNode) {
|
|
38
|
+
const Wrapper = wrapper({ user: account() });
|
|
39
|
+
return render(
|
|
40
|
+
<Wrapper>
|
|
41
|
+
<ForgeThemeProvider theme={theme}>{node}</ForgeThemeProvider>
|
|
42
|
+
</Wrapper>,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Composer ─────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe("CommunityComposer with a poll", () => {
|
|
49
|
+
const openPoll = () => {
|
|
50
|
+
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
51
|
+
installAdapter([]);
|
|
52
|
+
mount(<CommunityComposer allowPoll onSubmit={onSubmit} />);
|
|
53
|
+
fireEvent.click(screen.getByRole("button", { name: "Poll" }));
|
|
54
|
+
return onSubmit;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
it("offers no Poll toggle unless asked to (reply boxes)", () => {
|
|
58
|
+
installAdapter([]);
|
|
59
|
+
mount(<CommunityComposer onSubmit={vi.fn()} />);
|
|
60
|
+
expect(screen.queryByRole("button", { name: "Poll" })).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("the toggle reveals the question, two options, the switch, the close time and the results select", () => {
|
|
64
|
+
openPoll();
|
|
65
|
+
expect(screen.getByRole("button", { name: "Poll" }).getAttribute("aria-pressed")).toBe("true");
|
|
66
|
+
expect(screen.getByRole("textbox", { name: "Poll question" })).toBeTruthy();
|
|
67
|
+
expect(screen.getByRole("textbox", { name: "Option 1" })).toBeTruthy();
|
|
68
|
+
expect(screen.getByRole("textbox", { name: "Option 2" })).toBeTruthy();
|
|
69
|
+
expect(screen.getByRole("switch", { name: "Allow more than one answer" })).toBeTruthy();
|
|
70
|
+
expect(screen.getByLabelText("Closes (optional)")).toBeTruthy();
|
|
71
|
+
expect((screen.getByRole("combobox", { name: "Show results" }) as HTMLSelectElement).value).toBe("after_vote");
|
|
72
|
+
// Two options is the floor: neither can be removed yet.
|
|
73
|
+
expect((screen.getByRole("button", { name: "Remove option 1" }) as HTMLButtonElement).disabled).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("adds options up to 10 and removes them down to 2", () => {
|
|
77
|
+
openPoll();
|
|
78
|
+
const add = screen.getByRole("button", { name: "Add option" }) as HTMLButtonElement;
|
|
79
|
+
for (let i = 0; i < 9; i++) fireEvent.click(add);
|
|
80
|
+
expect(screen.getAllByRole("textbox", { name: /^Option \d+$/ })).toHaveLength(10);
|
|
81
|
+
expect(add.disabled).toBe(true);
|
|
82
|
+
fireEvent.click(screen.getByRole("button", { name: "Remove option 10" }));
|
|
83
|
+
expect(screen.getAllByRole("textbox", { name: /^Option \d+$/ })).toHaveLength(9);
|
|
84
|
+
expect(add.disabled).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("shows every error visibly and sends nothing while the poll is incomplete", async () => {
|
|
88
|
+
const onSubmit = openPoll();
|
|
89
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 1" }), { target: { value: "Yes" } });
|
|
90
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 2" }), { target: { value: " yes " } });
|
|
91
|
+
fireEvent.click(screen.getByRole("button", { name: "Post" }));
|
|
92
|
+
|
|
93
|
+
const alerts = screen.getAllByRole("alert").map((a) => a.textContent);
|
|
94
|
+
expect(alerts).toContain("Add a question. It becomes the post title.");
|
|
95
|
+
expect(alerts).toContain("This option repeats another one.");
|
|
96
|
+
expect(screen.getByRole("textbox", { name: "Option 2" }).getAttribute("aria-invalid")).toBe("true");
|
|
97
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("a close time in the past is refused", () => {
|
|
101
|
+
const onSubmit = openPoll();
|
|
102
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Poll question" }), { target: { value: "Which day?" } });
|
|
103
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 1" }), { target: { value: "Mon" } });
|
|
104
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 2" }), { target: { value: "Tue" } });
|
|
105
|
+
fireEvent.change(screen.getByLabelText("Closes (optional)"), { target: { value: "2020-01-01T10:00" } });
|
|
106
|
+
fireEvent.click(screen.getByRole("button", { name: "Post" }));
|
|
107
|
+
expect(screen.getByRole("alert").textContent).toBe("The closing time must be in the future.");
|
|
108
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("sends the question as the title and the poll, with no body text needed", async () => {
|
|
112
|
+
const onSubmit = openPoll();
|
|
113
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Poll question" }), { target: { value: " Which day? " } });
|
|
114
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 1" }), { target: { value: " Mon " } });
|
|
115
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 2" }), { target: { value: "Tue" } });
|
|
116
|
+
fireEvent.click(screen.getByRole("button", { name: "Add option" }));
|
|
117
|
+
fireEvent.change(screen.getByRole("textbox", { name: "Option 3" }), { target: { value: "Wed" } });
|
|
118
|
+
fireEvent.click(screen.getByRole("switch", { name: "Allow more than one answer" }));
|
|
119
|
+
fireEvent.change(screen.getByLabelText("Closes (optional)"), { target: { value: "2099-01-01T10:00" } });
|
|
120
|
+
fireEvent.change(screen.getByRole("combobox", { name: "Show results" }), { target: { value: "after_close" } });
|
|
121
|
+
fireEvent.click(screen.getByRole("button", { name: "Post" }));
|
|
122
|
+
|
|
123
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
124
|
+
const sentInput = onSubmit.mock.calls[0][0];
|
|
125
|
+
expect(sentInput.title).toBe("Which day?");
|
|
126
|
+
expect(sentInput.poll).toEqual({
|
|
127
|
+
options: ["Mon", "Tue", "Wed"],
|
|
128
|
+
multipleChoice: true,
|
|
129
|
+
closesAt: new Date("2099-01-01T10:00").toISOString(),
|
|
130
|
+
resultsVisibility: "after_close",
|
|
131
|
+
});
|
|
132
|
+
expect(sentInput.attachments).toEqual([]);
|
|
133
|
+
// Posted: the poll folds away and starts fresh next time.
|
|
134
|
+
await waitFor(() => expect(screen.queryByTestId("community-composer-poll")).toBeNull());
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ── Poll card ────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const A = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
|
141
|
+
const B = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
142
|
+
|
|
143
|
+
const poll = (overrides: Partial<CommunityPollView> = {}): CommunityPollView => ({
|
|
144
|
+
id: "poll-1",
|
|
145
|
+
multipleChoice: false,
|
|
146
|
+
closesAt: null,
|
|
147
|
+
closed: false,
|
|
148
|
+
resultsVisibility: "after_vote",
|
|
149
|
+
resultsVisible: false,
|
|
150
|
+
totalVoters: null,
|
|
151
|
+
myVotes: [],
|
|
152
|
+
options: [
|
|
153
|
+
{ id: A, label: "Monday", position: 0, votes: null },
|
|
154
|
+
{ id: B, label: "Tuesday", position: 1, votes: null },
|
|
155
|
+
],
|
|
156
|
+
...overrides,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const shown = (overrides: Partial<CommunityPollView> = {}) =>
|
|
160
|
+
poll({
|
|
161
|
+
resultsVisible: true,
|
|
162
|
+
totalVoters: 4,
|
|
163
|
+
options: [
|
|
164
|
+
{ id: A, label: "Monday", position: 0, votes: 1 },
|
|
165
|
+
{ id: B, label: "Tuesday", position: 1, votes: 3 },
|
|
166
|
+
],
|
|
167
|
+
...overrides,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const cardPost = (p: CommunityPollView) => ({ id: "post-1", spaceId: "space-1", title: "Which day works?", poll: p });
|
|
171
|
+
|
|
172
|
+
describe("CommunityPollCard", () => {
|
|
173
|
+
it("an open single-choice poll: a named group of radios and a Vote button, results after voting", async () => {
|
|
174
|
+
installAdapter([post("/public/community/posts/post-1/poll/vote", () => shown({ myVotes: [B] }))]);
|
|
175
|
+
mount(<CommunityPollCard post={cardPost(poll())} />);
|
|
176
|
+
|
|
177
|
+
const group = screen.getByRole("group", { name: /Which day works\?/ });
|
|
178
|
+
const radios = within(group).getAllByRole("radio");
|
|
179
|
+
expect(radios).toHaveLength(2);
|
|
180
|
+
expect(screen.getByLabelText("Monday")).toBeTruthy();
|
|
181
|
+
expect(screen.getByTestId("community-poll-hidden").textContent).toBe("Results after you vote");
|
|
182
|
+
expect(screen.queryByTestId("community-poll-bar")).toBeNull();
|
|
183
|
+
|
|
184
|
+
const vote = screen.getByRole("button", { name: "Vote" }) as HTMLButtonElement;
|
|
185
|
+
expect(vote.disabled).toBe(true);
|
|
186
|
+
fireEvent.click(screen.getByLabelText("Tuesday"));
|
|
187
|
+
expect(vote.disabled).toBe(false);
|
|
188
|
+
fireEvent.click(vote);
|
|
189
|
+
|
|
190
|
+
await waitFor(() =>
|
|
191
|
+
expect(calls("post", "/public/community/posts/post-1/poll/vote").map((c) => c.body)).toEqual([
|
|
192
|
+
{ profileId: "profile-1", communityId: COMMUNITY.id, optionIds: [B] },
|
|
193
|
+
]),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("a multiple-choice poll uses checkboxes", () => {
|
|
198
|
+
installAdapter([]);
|
|
199
|
+
mount(<CommunityPollCard post={cardPost(poll({ multipleChoice: true }))} />);
|
|
200
|
+
expect(screen.getAllByRole("checkbox")).toHaveLength(2);
|
|
201
|
+
expect(screen.getByTestId("community-poll-status").textContent).toContain("Pick one or more");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("results held until the close say so, even after voting", () => {
|
|
205
|
+
installAdapter([]);
|
|
206
|
+
mount(
|
|
207
|
+
<CommunityPollCard
|
|
208
|
+
post={cardPost(poll({ resultsVisibility: "after_close", myVotes: [A], closesAt: "2099-01-01T10:00:00.000Z" }))}
|
|
209
|
+
/>,
|
|
210
|
+
);
|
|
211
|
+
expect(screen.getByTestId("community-poll-hidden").textContent).toBe("Results when the poll closes");
|
|
212
|
+
expect(screen.queryByText(/%$/)).toBeNull();
|
|
213
|
+
expect(screen.getByTestId("community-poll-status").textContent).toContain("Closes");
|
|
214
|
+
expect(screen.getByRole("button", { name: "Change vote" })).toBeTruthy();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("results shown: bars with percentages, the voter count, and the member's vote marked", () => {
|
|
218
|
+
installAdapter([]);
|
|
219
|
+
mount(<CommunityPollCard post={cardPost(shown({ myVotes: [B] }))} />);
|
|
220
|
+
expect(screen.getAllByTestId("community-poll-bar")).toHaveLength(2);
|
|
221
|
+
expect(screen.getByText("25%")).toBeTruthy();
|
|
222
|
+
expect(screen.getByText("75%")).toBeTruthy();
|
|
223
|
+
expect(screen.getByTestId("community-poll-status").textContent).toContain("4 voters");
|
|
224
|
+
expect(screen.getByText("(your vote)", { exact: false })).toBeTruthy();
|
|
225
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("change vote reopens the options, and withdraw sends an empty answer", async () => {
|
|
229
|
+
installAdapter([post("/public/community/posts/post-1/poll/vote", () => poll())]);
|
|
230
|
+
mount(<CommunityPollCard post={cardPost(shown({ myVotes: [B] }))} />);
|
|
231
|
+
|
|
232
|
+
fireEvent.click(screen.getByRole("button", { name: "Change vote" }));
|
|
233
|
+
// Results stay beside each option while choosing again ("Tuesday 75%").
|
|
234
|
+
expect((screen.getByLabelText(/^Tuesday/) as HTMLInputElement).checked).toBe(true);
|
|
235
|
+
// The same answer again is not a change.
|
|
236
|
+
expect((screen.getByRole("button", { name: "Update vote" }) as HTMLButtonElement).disabled).toBe(true);
|
|
237
|
+
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
238
|
+
|
|
239
|
+
fireEvent.click(screen.getByRole("button", { name: "Withdraw vote" }));
|
|
240
|
+
await waitFor(() =>
|
|
241
|
+
expect(calls("post", "/public/community/posts/post-1/poll/vote")[0]?.body).toMatchObject({ optionIds: [] }),
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("a closed poll shows its final state and takes no votes", () => {
|
|
246
|
+
installAdapter([]);
|
|
247
|
+
mount(<CommunityPollCard post={cardPost(shown({ closed: true, closesAt: "2026-01-01T00:00:00.000Z", myVotes: [A] }))} />);
|
|
248
|
+
expect(screen.getByTestId("community-poll-status").textContent).toContain("Poll closed");
|
|
249
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
250
|
+
expect(screen.queryByRole("button", { name: "Vote" })).toBeNull();
|
|
251
|
+
expect(screen.queryByRole("button", { name: "Change vote" })).toBeNull();
|
|
252
|
+
expect(screen.queryByRole("button", { name: "Withdraw vote" })).toBeNull();
|
|
253
|
+
expect(screen.getByText("25%")).toBeTruthy();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("a refused vote is shown to the member", async () => {
|
|
257
|
+
installAdapter([
|
|
258
|
+
post("/public/community/posts/post-1/poll/vote", () => {
|
|
259
|
+
throw { status: 409, message: "closed" };
|
|
260
|
+
}),
|
|
261
|
+
]);
|
|
262
|
+
mount(<CommunityPollCard post={cardPost(poll())} />);
|
|
263
|
+
fireEvent.click(screen.getByLabelText("Monday"));
|
|
264
|
+
fireEvent.click(screen.getByRole("button", { name: "Vote" }));
|
|
265
|
+
expect((await screen.findByRole("alert")).textContent).toBe("This poll has closed.");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("a post waiting for approval shows the poll read-only", () => {
|
|
269
|
+
installAdapter([]);
|
|
270
|
+
mount(<CommunityPollCard post={cardPost(poll())} disabled />);
|
|
271
|
+
expect(screen.queryByRole("radio")).toBeNull();
|
|
272
|
+
expect(screen.queryByRole("button", { name: "Vote" })).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── Level locks ──────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const space = (overrides: Record<string, unknown> = {}) => ({
|
|
279
|
+
id: "space-open",
|
|
280
|
+
profileId: "profile-1",
|
|
281
|
+
name: "General",
|
|
282
|
+
slug: "general",
|
|
283
|
+
description: null,
|
|
284
|
+
type: "feed",
|
|
285
|
+
courseId: null,
|
|
286
|
+
order: 0,
|
|
287
|
+
archivedAt: null,
|
|
288
|
+
createdAt: "2026-09-01T00:00:00.000Z",
|
|
289
|
+
tiers: [],
|
|
290
|
+
postCount: 3,
|
|
291
|
+
memberCount: 2,
|
|
292
|
+
isJoined: false,
|
|
293
|
+
minLevel: 1,
|
|
294
|
+
levelLocked: false,
|
|
295
|
+
...overrides,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("CommunitySpaces with a level-locked space", () => {
|
|
299
|
+
it("lists it locked with its level, not as a way in", async () => {
|
|
300
|
+
installAdapter([
|
|
301
|
+
get("/public/community/spaces", () => [
|
|
302
|
+
space(),
|
|
303
|
+
space({ id: "space-locked", name: "Inner circle", slug: "inner", minLevel: 3, levelLocked: true }),
|
|
304
|
+
]),
|
|
305
|
+
]);
|
|
306
|
+
const onOpenSpace = vi.fn();
|
|
307
|
+
mount(<CommunitySpaces onOpenSpace={onOpenSpace} />);
|
|
308
|
+
|
|
309
|
+
const rows = await screen.findAllByTestId("community-space-row");
|
|
310
|
+
expect(rows).toHaveLength(2);
|
|
311
|
+
const [open, locked] = rows;
|
|
312
|
+
expect(open.getAttribute("role")).toBe("button");
|
|
313
|
+
expect(locked.getAttribute("role")).toBeNull();
|
|
314
|
+
expect(locked.getAttribute("tabindex")).toBeNull();
|
|
315
|
+
expect(within(locked).getByTestId("community-space-level-lock").textContent).toBe("Unlocks at level 3");
|
|
316
|
+
expect(within(locked).queryByRole("button", { name: "Join" })).toBeNull();
|
|
317
|
+
|
|
318
|
+
fireEvent.click(locked);
|
|
319
|
+
fireEvent.keyDown(locked, { key: "Enter" });
|
|
320
|
+
expect(onOpenSpace).not.toHaveBeenCalled();
|
|
321
|
+
fireEvent.click(open);
|
|
322
|
+
expect(onOpenSpace).toHaveBeenCalledWith(expect.objectContaining({ id: "space-open" }));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("CommunityWiki and a page a level opens", () => {
|
|
327
|
+
const pages = [
|
|
328
|
+
{ id: "p1", title: "Start here", slug: "start", isPublic: false, locked: false, unlockTiers: [], unlockLevel: null, updatedAt: "2026-09-01" },
|
|
329
|
+
{ id: "p2", title: "Level 4 notes", slug: "level-4", isPublic: false, locked: true, unlockTiers: [], unlockLevel: 4, updatedAt: "2026-09-01" },
|
|
330
|
+
{
|
|
331
|
+
id: "p3",
|
|
332
|
+
title: "Gold vault",
|
|
333
|
+
slug: "gold",
|
|
334
|
+
isPublic: false,
|
|
335
|
+
locked: true,
|
|
336
|
+
unlockTiers: [{ id: "tier-gold", name: "Gold" }],
|
|
337
|
+
unlockLevel: 5,
|
|
338
|
+
updatedAt: "2026-09-01",
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
it("says the level in the list and on the page, with no upgrade to buy", async () => {
|
|
343
|
+
installAdapter([get(`/public/communities/${COMMUNITY.slug}/wiki`, () => pages)]);
|
|
344
|
+
mount(<CommunityWiki pageSlug="level-4" getPageHref={(slug) => `/wiki/${slug}`} />);
|
|
345
|
+
expect((await screen.findByTestId("community-wiki-unlock")).textContent).toBe("Unlocks at level 4.");
|
|
346
|
+
expect(screen.queryByTestId("community-wiki-upgrade")).toBeNull();
|
|
347
|
+
expect(screen.getAllByTestId("community-wiki-item-level").map((el) => el.textContent)).toEqual([
|
|
348
|
+
"Unlocks at level 4.",
|
|
349
|
+
"Unlocks at level 5. This page is part of Gold.",
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("names the tiers beside the level, and offers the upgrade", async () => {
|
|
354
|
+
installAdapter([get(`/public/communities/${COMMUNITY.slug}/wiki`, () => pages)]);
|
|
355
|
+
mount(<CommunityWiki pageSlug="gold" getPageHref={(slug) => `/wiki/${slug}`} />);
|
|
356
|
+
expect((await screen.findByTestId("community-wiki-unlock")).textContent).toBe(
|
|
357
|
+
"Unlocks at level 5. This page is part of Gold.",
|
|
358
|
+
);
|
|
359
|
+
expect(screen.getByTestId("community-wiki-upgrade")).toBeTruthy();
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("CommunityNotificationsInbox and a level-up", () => {
|
|
364
|
+
it("says the level the member reached", async () => {
|
|
365
|
+
installAdapter([
|
|
366
|
+
get("/public/community/notifications", () => ({
|
|
367
|
+
data: [
|
|
368
|
+
{
|
|
369
|
+
id: "n1",
|
|
370
|
+
profileId: "profile-1",
|
|
371
|
+
accountId: "account-1",
|
|
372
|
+
type: "level_up",
|
|
373
|
+
entityType: "community",
|
|
374
|
+
entityId: COMMUNITY.id,
|
|
375
|
+
readAt: null,
|
|
376
|
+
createdAt: new Date().toISOString(),
|
|
377
|
+
data: { level: 5 },
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
unreadCount: 1,
|
|
381
|
+
})),
|
|
382
|
+
]);
|
|
383
|
+
mount(<CommunityNotificationsInbox onOpenPost={vi.fn()} />);
|
|
384
|
+
expect((await screen.findByTestId("community-notification-title")).textContent).toBe("You reached level 5");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Community styled blocks. NOTE: lane-local aggregator
|
|
1
|
+
// Community styled blocks. NOTE: lane-local aggregator. The integrator
|
|
2
2
|
// re-exports it from `ui/index.ts` (`export * from "./styled/community"`).
|
|
3
3
|
export { CommunitySpaces, type CommunitySpacesProps } from "./CommunitySpaces";
|
|
4
4
|
export { CommunityFeed, type CommunityFeedProps } from "./CommunityFeed";
|
|
@@ -10,7 +10,11 @@ export {
|
|
|
10
10
|
COMMUNITY_REACTION_EMOJIS,
|
|
11
11
|
type CommunityReactionBarProps,
|
|
12
12
|
} from "./CommunityReactionBar";
|
|
13
|
-
export {
|
|
13
|
+
export {
|
|
14
|
+
CommunityRichText,
|
|
15
|
+
type CommunityRichTextProps,
|
|
16
|
+
type CommunityRichTextPrivateImage,
|
|
17
|
+
} from "./CommunityRichText";
|
|
14
18
|
export { CommunityMediaGallery } from "./CommunityMediaGallery";
|
|
15
19
|
export { CommunityMemberProfile, type CommunityMemberProfileProps } from "./CommunityMemberProfile";
|
|
16
20
|
export {
|
|
@@ -21,3 +25,34 @@ export {
|
|
|
21
25
|
} from "./CommunityNotifications";
|
|
22
26
|
export { CommunityLeaderboard, type CommunityLeaderboardProps } from "./CommunityLeaderboard";
|
|
23
27
|
export { MemberAvatar, LevelBadge, type MemberAvatarProps } from "./MemberAvatar";
|
|
28
|
+
export { CommunityModeration, type CommunityModerationProps } from "./CommunityModeration";
|
|
29
|
+
export { CommunityMembers, type CommunityMembersProps } from "./CommunityMembers";
|
|
30
|
+
export { CommunityMemberGlobe, type CommunityMemberGlobeProps } from "./CommunityMemberGlobe";
|
|
31
|
+
export {
|
|
32
|
+
CommunityModeratorMenu,
|
|
33
|
+
type CommunityModeratorMenuProps,
|
|
34
|
+
type CommunityModeratorMenuTarget,
|
|
35
|
+
} from "./CommunityModeratorMenu";
|
|
36
|
+
export {
|
|
37
|
+
CommunityPendingBadge,
|
|
38
|
+
CommunityRemovedNotice,
|
|
39
|
+
type CommunityPendingBadgeProps,
|
|
40
|
+
type CommunityRemovedNoticeProps,
|
|
41
|
+
type CommunityRemovedNoticeTarget,
|
|
42
|
+
} from "./CommunityModerationState";
|
|
43
|
+
export { CommunityPollCard, type CommunityPollCardProps } from "./CommunityPollCard";
|
|
44
|
+
export { CommunityLinkPreview, type CommunityLinkPreviewProps } from "./CommunityLinkPreview";
|
|
45
|
+
export {
|
|
46
|
+
CommunityAgentBadge,
|
|
47
|
+
CommunityAgentAvatar,
|
|
48
|
+
CommunityAgentName,
|
|
49
|
+
CommunityAgentThinking,
|
|
50
|
+
CommunityAgentFeedback,
|
|
51
|
+
CommunityCatchMeUp,
|
|
52
|
+
CommunityLinkifiedText,
|
|
53
|
+
type CommunityAgentIdentity,
|
|
54
|
+
type CommunityAgentAvatarProps,
|
|
55
|
+
type CommunityAgentNameProps,
|
|
56
|
+
type CommunityAgentFeedbackProps,
|
|
57
|
+
type CommunityCatchMeUpProps,
|
|
58
|
+
} from "./CommunityAgent";
|