fix: Use origin as provided in HeadSeo. origin would be different for Domain cases (#14935)
* fix: Use origin as provided in HeadSeo. origin would be different for Domain cases * Add a test * add more test * Add tests * Cleanup tests * Add an e2e as well * fix-test * Reset mocks
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
@@ -159,8 +160,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
}
|
||||
|
||||
const { currentOrgDomain } = orgDomainConfig(context.req);
|
||||
return {
|
||||
props: {
|
||||
orgSlug: currentOrgDomain,
|
||||
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
|
||||
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
|
||||
profile,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import Success from "./bookings-single-view";
|
||||
|
||||
function mockedSuccessComponentProps(props: Partial<React.ComponentProps<typeof Success>>) {
|
||||
return {
|
||||
eventType: {
|
||||
id: 1,
|
||||
title: "Event Title",
|
||||
description: "",
|
||||
locations: null,
|
||||
length: 15,
|
||||
userId: null,
|
||||
eventName: "d",
|
||||
timeZone: null,
|
||||
recurringEvent: null,
|
||||
requiresConfirmation: false,
|
||||
disableGuests: false,
|
||||
seatsPerTimeSlot: null,
|
||||
seatsShowAttendees: null,
|
||||
seatsShowAvailabilityCount: null,
|
||||
schedulingType: null,
|
||||
price: 0,
|
||||
currency: "usd",
|
||||
successRedirectUrl: null,
|
||||
customInputs: [],
|
||||
team: null,
|
||||
workflows: [],
|
||||
hosts: [],
|
||||
users: [],
|
||||
owner: null,
|
||||
isDynamic: false,
|
||||
periodStartDate: "1",
|
||||
periodEndDate: "1",
|
||||
metadata: null,
|
||||
bookingFields: [] as unknown as [] & z.BRAND<"HAS_SYSTEM_FIELDS">,
|
||||
},
|
||||
profile: {
|
||||
name: "John",
|
||||
email: null,
|
||||
theme: null,
|
||||
brandColor: null,
|
||||
darkBrandColor: null,
|
||||
slug: null,
|
||||
},
|
||||
bookingInfo: {
|
||||
uid: "uid",
|
||||
metadata: null,
|
||||
customInputs: [],
|
||||
startTime: new Date(),
|
||||
endTime: new Date(),
|
||||
id: 1,
|
||||
user: null,
|
||||
eventType: null,
|
||||
seatsReferences: [],
|
||||
userPrimaryEmail: null,
|
||||
eventTypeId: null,
|
||||
title: "Booking Title",
|
||||
description: null,
|
||||
location: null,
|
||||
recurringEventId: null,
|
||||
smsReminderNumber: "0",
|
||||
cancellationReason: null,
|
||||
rejectionReason: null,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
attendees: [],
|
||||
responses: {
|
||||
name: "John",
|
||||
},
|
||||
},
|
||||
orgSlug: null,
|
||||
userTimeFormat: 12,
|
||||
requiresLoginToUpdate: false,
|
||||
themeBasis: "dark",
|
||||
hideBranding: false,
|
||||
recurringBookings: null,
|
||||
trpcState: {
|
||||
queries: [],
|
||||
mutations: [],
|
||||
},
|
||||
dynamicEventName: "Event Title",
|
||||
paymentStatus: null,
|
||||
...props,
|
||||
} satisfies React.ComponentProps<typeof Success>;
|
||||
}
|
||||
|
||||
describe("Success Component", () => {
|
||||
it("renders HeadSeo correctly", () => {
|
||||
vi.mocked(getOrgFullOrigin).mockImplementation((text: string | null) => `${text}.cal.local`);
|
||||
vi.mocked(useRouterQuery).mockReturnValue({
|
||||
uid: "uid",
|
||||
});
|
||||
vi.mocked(useSession).mockReturnValue({
|
||||
update: vi.fn(),
|
||||
status: "authenticated",
|
||||
data: {
|
||||
hasValidLicense: true,
|
||||
upId: "1",
|
||||
expires: "1",
|
||||
user: {
|
||||
name: "John",
|
||||
id: 1,
|
||||
profile: {
|
||||
id: null,
|
||||
upId: "1",
|
||||
username: null,
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockObject = {
|
||||
props: mockedSuccessComponentProps({
|
||||
orgSlug: "org1",
|
||||
}),
|
||||
};
|
||||
|
||||
render(<Success {...mockObject.props} />);
|
||||
|
||||
const expectedTitle = `booking_confirmed`;
|
||||
const expectedDescription = expectedTitle;
|
||||
expect(HeadSeo).toHaveBeenCalledWith(
|
||||
{
|
||||
origin: `${mockObject.props.orgSlug}.cal.local`,
|
||||
title: expectedTitle,
|
||||
description: expectedDescription,
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import type { nameObjectSchema } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import type { ConfigType } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
@@ -60,12 +61,10 @@ import {
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { timeZone } from "@lib/clock";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import CancelBooking from "@components/booking/CancelBooking";
|
||||
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||
import PageWrapper from "@calcom/web/components/PageWrapper";
|
||||
import CancelBooking from "@calcom/web/components/booking/CancelBooking";
|
||||
import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema";
|
||||
import { timeZone } from "@calcom/web/lib/clock";
|
||||
|
||||
import type { PageProps } from "./bookings-single-view.getServerSideProps";
|
||||
|
||||
@@ -109,7 +108,7 @@ export default function Success(props: PageProps) {
|
||||
const routerQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate } = props;
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate, orgSlug } = props;
|
||||
|
||||
const {
|
||||
allRemainingBookings,
|
||||
@@ -377,7 +376,7 @@ export default function Success(props: PageProps) {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<HeadSeo title={title} description={title} />
|
||||
<HeadSeo origin={getOrgFullOrigin(orgSlug)} title={title} description={title} />
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import React from "react";
|
||||
import { vi, afterEach } from "vitest";
|
||||
|
||||
global.React = React;
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
// Mock all modules that are used in multiple tests for modules
|
||||
// We don't intend to provide the mock implementation here. They should be provided by respective tests.
|
||||
// But it makes it super easy to start testing any module view without worrying about mocking the dependencies.
|
||||
vi.mock("next-auth/react", () => ({
|
||||
useSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: vi.fn().mockReturnValue({
|
||||
replace: vi.fn(),
|
||||
}),
|
||||
usePathname: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/app-store/BookingPageTagManager", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/app-store/locations", () => ({
|
||||
DailyLocationType: "daily",
|
||||
guessEventLocationType: vi.fn(),
|
||||
getSuccessPageLocationMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/app-store/utils", () => ({
|
||||
getEventTypeAppData: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/core/event", () => ({
|
||||
getEventName: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/ee/organizations/lib/orgDomains", () => ({
|
||||
getOrgFullOrigin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/features/eventtypes/components", () => ({
|
||||
EventTypeDescriptionLazy: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/embed-core/embed-iframe", () => {
|
||||
return {
|
||||
useIsBackgroundTransparent: vi.fn(),
|
||||
useIsEmbed: vi.fn(),
|
||||
useEmbedNonStylesConfig: vi.fn(),
|
||||
useEmbedStyles: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/features/bookings/components/event-meta/Price", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/features/bookings/lib/SystemField", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => {
|
||||
return {
|
||||
DEFAULT_LIGHT_BRAND_COLOR: "DEFAULT_LIGHT_BRAND_COLOR",
|
||||
DEFAULT_DARK_BRAND_COLOR: "DEFAULT_DARK_BRAND_COLOR",
|
||||
BOOKER_NUMBER_OF_DAYS_TO_LOAD: 1,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/date-fns", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/getBrandColours", () => {
|
||||
return {
|
||||
default: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useCompatSearchParams", () => {
|
||||
return {
|
||||
useCompatSearchParams: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => {
|
||||
return {
|
||||
useLocale: vi.fn().mockReturnValue({
|
||||
t: vi.fn().mockImplementation((text: string) => {
|
||||
return text;
|
||||
}),
|
||||
i18n: {
|
||||
language: "en",
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useRouterQuery", () => {
|
||||
return {
|
||||
useRouterQuery: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useTheme", () => {
|
||||
return {
|
||||
default: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/recurringStrings", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/recurringStrings", () => {
|
||||
return {};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/prisma/zod-utils", () => ({
|
||||
BookerLayouts: {
|
||||
MONTH_VIEW: "month",
|
||||
},
|
||||
EventTypeMetaDataSchema: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
bookingMetadataSchema: {
|
||||
parse: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/trpc/react", () => ({
|
||||
trpc: {
|
||||
viewer: {
|
||||
public: {
|
||||
submitRating: {
|
||||
useMutation: vi.fn(),
|
||||
},
|
||||
noShow: {
|
||||
useMutation: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/ui", () => ({
|
||||
HeadSeo: vi.fn(),
|
||||
useCalcomTheme: vi.fn(),
|
||||
Icon: vi.fn(),
|
||||
UnpublishedEntity: vi.fn(),
|
||||
UserAvatar: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/web/components/PageWrapper", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/web/components/booking/CancelBooking", () => ({}));
|
||||
|
||||
vi.mock("@calcom/web/components/schemas/EventReservationSchema", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/web/lib/clock", () => ({
|
||||
timeZone: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./bookings-single-view.getServerSideProps", () => ({}));
|
||||
|
||||
vi.mock("@calcom/lib/webstorage", () => ({
|
||||
localStorage: {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/timeFormat", () => ({
|
||||
detectBrowserTimeFormat: vi.fn(),
|
||||
isBrowserLocale24h: vi.fn(),
|
||||
getIs24hClockFromLocalStorage: vi.fn(),
|
||||
}));
|
||||
@@ -0,0 +1,102 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import UserPage from "./users-public-view";
|
||||
|
||||
function mockedUserPageComponentProps(props: Partial<React.ComponentProps<typeof UserPage>>) {
|
||||
return {
|
||||
trpcState: {
|
||||
mutations: [],
|
||||
queries: [],
|
||||
},
|
||||
themeBasis: "dark",
|
||||
safeBio: "My Bio",
|
||||
profile: {
|
||||
name: "John Doe",
|
||||
image: "john-profile-url",
|
||||
theme: "dark",
|
||||
brandColor: "red",
|
||||
darkBrandColor: "black",
|
||||
organization: { requestedSlug: "slug", slug: "slug", id: 1 },
|
||||
allowSEOIndexing: true,
|
||||
username: "john",
|
||||
},
|
||||
users: [
|
||||
{
|
||||
name: "John Doe",
|
||||
username: "john",
|
||||
avatarUrl: "john-user-url",
|
||||
bio: "",
|
||||
verified: false,
|
||||
profile: {
|
||||
upId: "1",
|
||||
id: 1,
|
||||
username: "john",
|
||||
organizationId: null,
|
||||
organization: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
markdownStrippedBio: "My Bio",
|
||||
entity: {
|
||||
considerUnpublished: false,
|
||||
...(props.entity ?? null),
|
||||
},
|
||||
eventTypes: [],
|
||||
} satisfies React.ComponentProps<typeof UserPage>;
|
||||
}
|
||||
|
||||
describe("UserPage Component", () => {
|
||||
it("should render HeadSeo with correct props", () => {
|
||||
const mockData = {
|
||||
props: mockedUserPageComponentProps({
|
||||
entity: {
|
||||
considerUnpublished: false,
|
||||
orgSlug: "org1",
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mocked(getOrgFullOrigin).mockImplementation((orgSlug: string | null) => {
|
||||
return `${orgSlug}.cal.local`;
|
||||
});
|
||||
|
||||
vi.mocked(useRouterQuery).mockReturnValue({
|
||||
uid: "uid",
|
||||
});
|
||||
|
||||
render(<UserPage {...mockData.props} />);
|
||||
|
||||
const expectedDescription = mockData.props.markdownStrippedBio;
|
||||
const expectedTitle = expectedDescription;
|
||||
expect(HeadSeo).toHaveBeenCalledWith(
|
||||
{
|
||||
origin: `${mockData.props.entity.orgSlug}.cal.local`,
|
||||
title: `${mockData.props.profile.name}`,
|
||||
description: expectedDescription,
|
||||
meeting: {
|
||||
profile: {
|
||||
name: mockData.props.profile.name,
|
||||
image: mockData.props.users[0].avatarUrl,
|
||||
},
|
||||
title: expectedTitle,
|
||||
users: [
|
||||
{
|
||||
name: mockData.props.users[0].name,
|
||||
username: mockData.props.users[0].username,
|
||||
},
|
||||
],
|
||||
},
|
||||
nextSeoProps: {
|
||||
nofollow: !mockData.props.profile.allowSEOIndexing,
|
||||
noindex: !mockData.props.profile.allowSEOIndexing,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useEmbedStyles,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -59,10 +60,10 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
|
||||
const isEventListEmpty = eventTypes.length === 0;
|
||||
const isOrg = !!user?.profile?.organization;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
|
||||
title={profile.name}
|
||||
description={markdownStrippedBio}
|
||||
meeting={{
|
||||
|
||||
@@ -12,6 +12,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { getOrgOrTeamAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -167,6 +168,7 @@ function TeamPage({
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(currentOrgDomain)}
|
||||
title={teamName}
|
||||
description={teamName}
|
||||
meeting={{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange } from "@calcom/types/schedule";
|
||||
@@ -42,7 +43,11 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => {
|
||||
|
||||
const titleText = document.querySelector("title")?.textContent;
|
||||
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content");
|
||||
const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content");
|
||||
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href");
|
||||
expect(titleText).toContain(name);
|
||||
expect(ogUrl).toEqual(`${WEBAPP_URL}/${user.username}/30-min`);
|
||||
expect(canonicalLink).toEqual(`${WEBAPP_URL}/${user.username}/30-min`);
|
||||
// Verify that there is correct URL that would generate the awesome OG image
|
||||
expect(ogImage).toContain(
|
||||
"/_next/image?w=1200&q=100&url=%2Fapi%2Fsocial%2Fog%2Fimage%3Ftype%3Dmeeting%26title%3D"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
@@ -16,6 +18,16 @@ import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
|
||||
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";
|
||||
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";
|
||||
|
||||
function getOrgOrigin(orgSlug: string | null) {
|
||||
if (!orgSlug) {
|
||||
throw new Error("orgSlug is required");
|
||||
}
|
||||
|
||||
let orgOrigin = WEBAPP_URL.replace("://app", `://${orgSlug}`);
|
||||
orgOrigin = orgOrigin.includes(orgSlug) ? orgOrigin : WEBAPP_URL.replace("://", `://${orgSlug}.`);
|
||||
return orgOrigin;
|
||||
}
|
||||
|
||||
test.describe("Bookings", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
@@ -187,6 +199,7 @@ test.describe("Bookings", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("User Event with same slug as another user's", () => {
|
||||
test("booking is created for first user when first user is booked", async ({ page, users, orgs }) => {
|
||||
const org = await orgs.create({
|
||||
@@ -251,6 +264,55 @@ test.describe("Bookings", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("check SSR and OG ", async ({ page, users, orgs }) => {
|
||||
const name = "Test User";
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
|
||||
const user = await users.create({
|
||||
name,
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
|
||||
const firstEventType = await user.getFirstEventAsOwner();
|
||||
const calLink = `/${user.username}/${firstEventType.slug}`;
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
const [response] = await Promise.all([
|
||||
// This promise resolves to the main resource response
|
||||
page.waitForResponse(
|
||||
(response) => response.url().includes(`${calLink}`) && response.status() === 200
|
||||
),
|
||||
|
||||
// Trigger the page navigation
|
||||
page.goto(`${calLink}`),
|
||||
]);
|
||||
const ssrResponse = await response.text();
|
||||
const document = new JSDOM(ssrResponse).window.document;
|
||||
const orgOrigin = getOrgOrigin(org.slug);
|
||||
const titleText = document.querySelector("title")?.textContent;
|
||||
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content");
|
||||
const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content");
|
||||
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href");
|
||||
expect(titleText).toContain(name);
|
||||
expect(ogUrl).toEqual(`${orgOrigin}${calLink}`);
|
||||
expect(canonicalLink).toEqual(`${orgOrigin}${calLink}`);
|
||||
// Verify that there is correct URL that would generate the awesome OG image
|
||||
expect(ogImage).toContain(
|
||||
"/_next/image?w=1200&q=100&url=%2Fapi%2Fsocial%2Fog%2Fimage%3Ftype%3Dmeeting%26title%3D"
|
||||
);
|
||||
// Verify Organizer Name in the URL
|
||||
expect(ogImage).toContain("meetingProfileName%3DTest%2520User%26");
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Scenario with same username in and outside organization", () => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import { BookerSeo } from "./BookerSeo";
|
||||
|
||||
// Mocking necessary modules and hooks
|
||||
vi.mock("@calcom/trpc/react", () => ({
|
||||
trpc: {
|
||||
viewer: {
|
||||
public: {
|
||||
event: {
|
||||
useQuery: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/ee/organizations/lib/orgDomains", () => ({
|
||||
getOrgFullOrigin: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/ui", () => ({
|
||||
HeadSeo: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("BookerSeo Component", () => {
|
||||
it("renders HeadSeo with correct props", () => {
|
||||
const mockData = {
|
||||
event: {
|
||||
slug: "event-slug",
|
||||
profile: { name: "John Doe", image: "image-url", username: "john" },
|
||||
title: "30min",
|
||||
hidden: false,
|
||||
users: [{ name: "Jane Doe", username: "jane" }],
|
||||
},
|
||||
entity: { fromRedirectOfNonOrgLink: false, orgSlug: "org1" },
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
trpc.viewer.public.event.useQuery.mockReturnValueOnce({
|
||||
data: mockData.event,
|
||||
});
|
||||
|
||||
vi.mocked(getOrgFullOrigin).mockImplementation((text: string | null) => `${text}.cal.local`);
|
||||
|
||||
render(
|
||||
<BookerSeo
|
||||
username={mockData.event.profile.username}
|
||||
eventSlug={mockData.event.slug}
|
||||
rescheduleUid={undefined}
|
||||
entity={mockData.entity}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(HeadSeo).toHaveBeenCalledWith(
|
||||
{
|
||||
origin: `${mockData.entity.orgSlug}.cal.local`,
|
||||
isBrandingHidden: undefined,
|
||||
// Don't know why we are adding space in the beginning
|
||||
title: ` ${mockData.event.title} | ${mockData.event.profile.name}`,
|
||||
description: ` ${mockData.event.title}`,
|
||||
meeting: {
|
||||
profile: {
|
||||
name: mockData.event.profile.name,
|
||||
image: mockData.event.profile.image,
|
||||
},
|
||||
title: mockData.event.title,
|
||||
users: [
|
||||
{
|
||||
name: mockData.event.users[0].name,
|
||||
username: mockData.event.users[0].username,
|
||||
},
|
||||
],
|
||||
},
|
||||
nextSeoProps: {
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
},
|
||||
},
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -47,6 +48,7 @@ export const BookerSeo = (props: BookerSeoProps) => {
|
||||
const title = event?.title ?? "";
|
||||
return (
|
||||
<HeadSeo
|
||||
origin={getOrgFullOrigin(entity.orgSlug ?? null)}
|
||||
title={`${rescheduleUid && !!bookingData ? t("reschedule") : ""} ${title} | ${profileName}`}
|
||||
description={`${rescheduleUid ? t("reschedule") : ""} ${title}`}
|
||||
meeting={{
|
||||
|
||||
@@ -110,9 +110,10 @@ export function subdomainSuffix() {
|
||||
return urlSplit.length === 3 ? urlSplit.slice(1).join(".") : urlSplit.join(".");
|
||||
}
|
||||
|
||||
export function getOrgFullOrigin(slug: string, options: { protocol: boolean } = { protocol: true }) {
|
||||
export function getOrgFullOrigin(slug: string | null, options: { protocol: boolean } = { protocol: true }) {
|
||||
if (!slug)
|
||||
return options.protocol ? WEBSITE_URL : WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||
|
||||
const orgFullOrigin = `${
|
||||
options.protocol ? `${new URL(WEBSITE_URL).protocol}//` : ""
|
||||
}${slug}.${subdomainSuffix()}`;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getOrgSlug, getOrgDomainConfigFromHostname } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import {
|
||||
getOrgSlug,
|
||||
getOrgDomainConfigFromHostname,
|
||||
getOrgFullOrigin,
|
||||
} from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import * as constants from "@calcom/lib/constants";
|
||||
|
||||
function setupEnvs({ WEBAPP_URL = "https://app.cal.com" } = {}) {
|
||||
function setupEnvs({ WEBAPP_URL = "https://app.cal.com", WEBSITE_URL } = {}) {
|
||||
Object.defineProperty(constants, "WEBAPP_URL", { value: WEBAPP_URL });
|
||||
Object.defineProperty(constants, "WEBSITE_URL", { value: WEBSITE_URL });
|
||||
Object.defineProperty(constants, "ALLOWED_HOSTNAMES", {
|
||||
value: ["cal.com", "cal.dev", "cal-staging.com", "cal.community", "cal.local:3000", "localhost:3000"],
|
||||
});
|
||||
@@ -82,4 +87,22 @@ describe("Org Domains Utils", () => {
|
||||
expect(getOrgSlug("acme.cal.com:3000")).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrgFullOrigin", () => {
|
||||
it("should return the regular(non-org) origin if slug is null", () => {
|
||||
setupEnvs({
|
||||
WEBAPP_URL: "https://app.cal.com",
|
||||
WEBSITE_URL: "https://abc.com",
|
||||
});
|
||||
expect(getOrgFullOrigin(null)).toEqual("https://abc.com");
|
||||
});
|
||||
it("should return the org origin if slug is set", () => {
|
||||
setupEnvs({
|
||||
WEBAPP_URL: "https://app.cal-app.com",
|
||||
WEBSITE_URL: "https://cal.com",
|
||||
});
|
||||
// We are supposed to use WEBAPP_URL to derive the origin from and not WEBSITE_URL
|
||||
expect(getOrgFullOrigin("org")).toEqual("https://org.cal-app.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ export type HeadSeoProps = {
|
||||
app?: AppImageProps;
|
||||
meeting?: MeetingImageProps;
|
||||
isBrandingHidden?: boolean;
|
||||
origin?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,10 +70,11 @@ const buildSeoMeta = (pageProps: {
|
||||
|
||||
export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
||||
const path = usePathname();
|
||||
|
||||
// The below code sets the defaultUrl for our canonical tags
|
||||
// Get the router's path
|
||||
// Set the default URL to either the current URL (if self-hosted) or https://cal.com canonical URL
|
||||
const defaultUrl = buildCanonical({ path, origin: CAL_URL });
|
||||
const defaultUrl = buildCanonical({ path, origin: props.origin || CAL_URL });
|
||||
|
||||
const {
|
||||
title,
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import type { NextSeoProps } from "next-seo";
|
||||
import type { OpenGraph } from "next/dist/lib/metadata/types/opengraph-types";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
import type { HeadSeoProps } from "./HeadSeo";
|
||||
import HeadSeo from "./HeadSeo";
|
||||
|
||||
vi.mock("next/navigation", () => {
|
||||
return {
|
||||
usePathname: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock("@calcom/lib/constants", () => {
|
||||
return {
|
||||
SEO_IMG_DEFAULT: "",
|
||||
SEO_IMG_OGIMG: "",
|
||||
APP_NAME: "Cal.com",
|
||||
CAL_URL: "http://cal.com",
|
||||
};
|
||||
});
|
||||
vi.mock("next-seo", () => {
|
||||
return {
|
||||
NextSeo: (props: NextSeoProps) => {
|
||||
@@ -16,7 +32,7 @@ vi.mock("next-seo", () => {
|
||||
image: images[0].url,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
return <title {...mockedProps} />;
|
||||
return <div id="mocked-next-seo" {...mockedProps} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -48,43 +64,97 @@ describe("Tests for HeadSeo component", () => {
|
||||
test("Should render mocked NextSeo", async () => {
|
||||
const { container } = render(<HeadSeo {...basicProps} />);
|
||||
await waitFor(async () => {
|
||||
const titleEl = container.querySelector("title");
|
||||
expect(titleEl?.getAttribute("canonical")).toEqual(basicProps.canonical);
|
||||
expect(titleEl?.getAttribute("description")).toEqual(basicProps.description);
|
||||
expect(titleEl?.getAttribute("site_name")).toEqual(basicProps.siteName);
|
||||
expect(titleEl?.getAttribute("image")).toContain("constructGenericImage");
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("canonical")).toEqual(basicProps.canonical);
|
||||
expect(mockedNextSeoEl?.getAttribute("description")).toEqual(basicProps.description);
|
||||
expect(mockedNextSeoEl?.getAttribute("site_name")).toEqual(basicProps.siteName);
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructGenericImage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Canonical Url", () => {
|
||||
test("Should provide `canonical` prop to NextSeo derived from `origin` prop if provided", async () => {
|
||||
const props = {
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
origin: "http://acme.cal.local",
|
||||
siteName: "Cal.com",
|
||||
};
|
||||
vi.mocked(usePathname).mockReturnValue("/mocked-path");
|
||||
const { container } = render(<HeadSeo {...props} />);
|
||||
await waitFor(async () => {
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("canonical")).toEqual(`${props.origin}/mocked-path`);
|
||||
expect(mockedNextSeoEl?.getAttribute("description")).toEqual(props.description);
|
||||
expect(mockedNextSeoEl?.getAttribute("site_name")).toEqual(props.siteName);
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructGenericImage");
|
||||
});
|
||||
});
|
||||
test("Should provide `canonical` prop to NextSeo derived from `CAL_URL` prop if origin not provided", async () => {
|
||||
const props = {
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
siteName: "Cal.com",
|
||||
};
|
||||
vi.mocked(usePathname).mockReturnValue("/mocked-path");
|
||||
const { container } = render(<HeadSeo {...props} />);
|
||||
await waitFor(async () => {
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("canonical")).toEqual(`${CAL_URL}/mocked-path`);
|
||||
expect(mockedNextSeoEl?.getAttribute("description")).toEqual(props.description);
|
||||
expect(mockedNextSeoEl?.getAttribute("site_name")).toEqual(props.siteName);
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructGenericImage");
|
||||
});
|
||||
});
|
||||
test("Should provide `canonical` prop to NextSeo from canonical prop if provided", async () => {
|
||||
const props = {
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
siteName: "Cal.com",
|
||||
origin: "http://acme.cal.local",
|
||||
canonical: "http://acme.cal.local/some-path",
|
||||
};
|
||||
vi.mocked(usePathname).mockReturnValue("/mocked-path");
|
||||
const { container } = render(<HeadSeo {...props} />);
|
||||
await waitFor(async () => {
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("canonical")).toEqual(props.canonical);
|
||||
expect(mockedNextSeoEl?.getAttribute("description")).toEqual(props.description);
|
||||
expect(mockedNextSeoEl?.getAttribute("site_name")).toEqual(props.siteName);
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructGenericImage");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render title with brand", async () => {
|
||||
const { container } = render(<HeadSeo {...basicProps} />);
|
||||
await waitFor(async () => {
|
||||
const titleEl = container.querySelector("title");
|
||||
expect(titleEl?.getAttribute("title")).toEqual(`${basicProps.title} | Cal.com`);
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("title")).toEqual(`${basicProps.title} | Cal.com`);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render title without brand", async () => {
|
||||
const { container } = render(<HeadSeo {...basicProps} isBrandingHidden />);
|
||||
await waitFor(async () => {
|
||||
const titleEl = container.querySelector("title");
|
||||
expect(titleEl?.getAttribute("title")).toEqual(`${basicProps.title}`);
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("title")).toEqual(`${basicProps.title}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render with app props", async () => {
|
||||
const { container } = render(<HeadSeo {...basicProps} app={{} as HeadSeoProps["app"]} />);
|
||||
await waitFor(async () => {
|
||||
const titleEl = container.querySelector("title");
|
||||
expect(titleEl?.getAttribute("image")).toContain("constructAppImage");
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructAppImage");
|
||||
});
|
||||
});
|
||||
|
||||
test("Should render with meeting props", async () => {
|
||||
const { container } = render(<HeadSeo {...basicProps} meeting={{} as HeadSeoProps["meeting"]} />);
|
||||
await waitFor(async () => {
|
||||
const titleEl = container.querySelector("title");
|
||||
expect(titleEl?.getAttribute("image")).toContain("constructMeetingImage");
|
||||
const mockedNextSeoEl = container.querySelector("#mocked-next-seo");
|
||||
expect(mockedNextSeoEl?.getAttribute("image")).toContain("constructMeetingImage");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,6 +146,15 @@ const workspaces = packagedEmbedTestsOnly
|
||||
setupFiles: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
name: "@calcom/web/modules/views",
|
||||
include: ["apps/web/modules/**/*.{test,spec}.tsx"],
|
||||
setupFiles: ["apps/web/modules/test-setup.ts"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default defineWorkspace(workspaces);
|
||||
|
||||
Reference in New Issue
Block a user