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:
Hariom Balhara
2024-05-23 11:47:46 +01:00
committed by GitHub
parent 29ccc58806
commit 5f5cf92456
16 changed files with 730 additions and 27 deletions
@@ -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 ")}>
+186
View File
@@ -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={{
+2
View File
@@ -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={{
+5
View File
@@ -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()}`;
+25 -2
View File
@@ -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");
});
});
});
+3 -1
View File
@@ -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");
});
});
});
+9
View File
@@ -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);