* ui playground * add types to ui * push avatar ignore ts and eslint errors * feat: V3 UI avatar (#18997) * avatar playground * add overflow-hidden back for loading indicator * update lock * feat: v3/badge (#18999) * badge intro * fix text on small badge * add dark mode warning * feat: v3/button (#19014) * TOC * wip buttons * shadows * secondary * semantic colours * minimal buttons * update disabled states * checkbox * dropdown v3 * load icons * fix design sections * fix icon gap * fix * remove client component from rsc * use peer as avatar * gif * fix button gap - dropdown iocn * support start icon in dropdowns * add alert demo + v3 component + darken semantic text * base inputs and text field * wip input * input field addon/suffix * toast page * wip select * match designs for input and select rounded * fix * fix select isloading transform * fix multi icons transition rotation * fix group heading * range slider demo + range slider * range slider popover design * v3 dialog * fix slider thumb size * wrap in suspense for useSearchParam difference in next14+ * base of tabs + horizontal tabs * fix select sizing to match input 36px * fix dark mode hover on button * target domState class for light/dark so we work on portal elements * fix default bg on popover * lightmode toasts + eleveation token * empty screen * intro to split button concept * mass colour updates * intro to themes * remove i18n from label with name * bouncy buttons * feat: small select * nav item dumb component * full demo * settings demo * fix tabs * fix mt * fix dropdown feedback from loom * alert fixes mentioned in loom * push font-cal * move to fumadocs * fix avatar * colors table * add typeography * icon + spacing guide * add border radius and shadow tables * fix colours and border radius * remove prose undline via fumas fork * fix icon buttons sizes * button group component * fix horizontal tabs * tab fixes * switches * pagination component * show toasts components raw * sync with airtable * add use client * 10px on input again * 10px in select also * fix input height * fix files merge * fix loading state for icon buttons * fix button fab * Enable addon to now overflow to next line in DS * use cal font for dialog text * add avatar square variant * fix select padding and menu size * fix type errors * fix type errors * add switch small size and fix type errors * fix ref types on input * update web styles * remove addonFilled to fix type errors * push dialog fixes * fix checkbox display * remove prose from alert examples * remove prose from all our component examples * badge rounded-lg on lg size * update colors * force w/h on icon buttons instead of inheriting * remove border-focus * remove border-focus from tailwind config * update tokens to match new colours * add new border subtle tokens and add to toasts * fix lg button padding * fix and drastically improve rtl rests * multi select use badges * use badge design for select pills * fix avatar alignment * fix tests and remove redudant ones * fix tests and remove redudant ones * update atoms globals css * extract tokens.css * extract app.css * force flex for atoms * add @calcom/ui/css * revert ui playground styles abstraction * fix input focus states * fix focus states for buttons * feat: v3/app work (#19233) * globals.css for app + event types listing search * few app wide NITS * fix select foucs within styling * fix formbuilder switch styles * fix disabled state of nested inputs * workflow fixes * webhook form fixes * i18n on tabs info * booking filters + tab * fix filters spacing * make first item so we get correct border radius if not default * round radio group in create event types + make button small * profile page * update editor br * button nits in existing places in the app * fix timezone select and multilist in guest form * fix text area on public booking page * size small event types create * margin on routing filter * filters improvments * routing insights fixes * fix date filter range * ooo fixes * fix teams/members bulk actions * create team UI * i18n in useTabs instead of in tab component now * Add event type setup border to availability * fix bulk actions on org members table * onboarding fixes * fix input types with ref stripe comparision * i18n fixes for platform * update globals of atoms * fix form builder firstname/lastname switch + correct footer useage * fix select * Revert "add @calcom/ui/css" This reverts commit 0dd8f9012907a278cfede4a98f289cb0744c9244. * revert tokens css * Revert "extract app.css" This reverts commit 10b998604816ad3310c24b36a02a638dabc5be24. * linear nits * workflow dropdown * fix page theme heading sizes to match cards * fix stepper for team creation * adjust margin to FilterSearchField * fix ooo icon * fix booker and dark mode due to darkgray 50 * more dark mode fixes * add size prop --------- Co-authored-by: Eunjae Lee <hey@eunjae.dev> Co-authored-by: Morgan Vernay <morgan@cal.com> * remove ui-playground * fix grow missing from merge * add search icon data-test-id back to search * fix FormAction style * fix datatestid for apps and tabs * change selector for team invitation e2e * fix use data-testid from item instaed of using names * rename upper case O * fix unit tests data-testid * fix unit tests data-testid * more data-testid changes * add data-testid to apps tab * data-testid to installed appPage * fix embed tests * fix filter-popover-trigger * use text-error and use dataId for locator * more e2e improvment * pass in single testId as button is now duplicate * show field-error instead * add test to eventtype tabs * Add css vars to embed too\ * add scrollbar and change dialog overlay colour * fix type * use attribute instead of class for tab detection * await promise * use new tokens for sprite generation * add correct testing Ids to e2e instead of button:text * use testIds for creation of orgs * Webhook receiver wait for 10 seconds --------- Co-authored-by: Eunjae Lee <hey@eunjae.dev> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Hariom <hariombalhara@gmail.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
525 lines
15 KiB
TypeScript
525 lines
15 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
import { Linter } from "eslint";
|
|
import { parse } from "node-html-parser";
|
|
|
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import { EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { MembershipRole } from "@calcom/prisma/client";
|
|
|
|
import { test } from "./lib/fixtures";
|
|
|
|
const linter = new Linter();
|
|
const eslintRules = {
|
|
"no-undef": "error",
|
|
"no-unused-vars": "off",
|
|
} as const;
|
|
test.describe.configure({ mode: "parallel" });
|
|
|
|
test.afterEach(({ users }) => users.deleteAll());
|
|
|
|
test.describe("Embed Code Generator Tests", () => {
|
|
test.describe("Non-Organization", () => {
|
|
test.beforeEach(async ({ users }) => {
|
|
const pro = await users.create();
|
|
await pro.apiLogin();
|
|
});
|
|
|
|
test.describe("Event Types Page", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/event-types");
|
|
});
|
|
|
|
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
|
|
const [pro] = users.get();
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "inline");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "inline",
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "inline",
|
|
orgSlug: null,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "inline",
|
|
orgSlug: null,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "inline",
|
|
calLink: `${pro.username}/multiple-duration`,
|
|
});
|
|
});
|
|
|
|
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
|
|
const [pro] = users.get();
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "floating-popup");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "floating-popup",
|
|
basePage: "/event-types",
|
|
});
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "floating-popup",
|
|
orgSlug: null,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "floating-popup",
|
|
orgSlug: null,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "floating-popup",
|
|
calLink: `${pro.username}/multiple-duration`,
|
|
});
|
|
});
|
|
|
|
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
|
|
const [pro] = users.get();
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "element-click");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "element-click",
|
|
basePage: "/event-types",
|
|
});
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "element-click",
|
|
orgSlug: null,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "element-click",
|
|
orgSlug: null,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "element-click",
|
|
calLink: `${pro.username}/multiple-duration`,
|
|
});
|
|
});
|
|
});
|
|
test.describe("Event Type Edit Page", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(`/event-types`);
|
|
await Promise.all([
|
|
page.locator('a[href*="/event-types/"]').first().click(),
|
|
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
|
|
]);
|
|
});
|
|
|
|
test("open Embed Dialog for the Event Type", async ({ page }) => {
|
|
const basePage = new URL(page.url()).pathname;
|
|
const embedUrl = await clickEmbedButton(page);
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage,
|
|
});
|
|
|
|
chooseEmbedType(page, "inline");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
basePage,
|
|
embedType: "inline",
|
|
});
|
|
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "inline",
|
|
orgSlug: null,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "inline",
|
|
calLink: decodeURIComponent(embedUrl),
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
test.describe("Organization", () => {
|
|
test.beforeEach(async ({ users, orgs }) => {
|
|
const org = await orgs.create({
|
|
name: "TestOrg",
|
|
});
|
|
const user = await users.create({
|
|
organizationId: org.id,
|
|
roleInOrganization: MembershipRole.MEMBER,
|
|
});
|
|
await user.apiLogin();
|
|
});
|
|
test.describe("Event Types Page", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto("/event-types");
|
|
});
|
|
|
|
test("open Embed Dialog and choose Inline for First Event Type", async ({ page, users }) => {
|
|
const [user] = users.get();
|
|
const { team: org } = await user.getOrgMembership();
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "inline");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "inline",
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
// Default tab is HTML code tab
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "inline",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "inline",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "inline",
|
|
calLink: `${user.username}/multiple-duration`,
|
|
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
|
|
});
|
|
});
|
|
|
|
test("open Embed Dialog and choose floating-popup for First Event Type", async ({ page, users }) => {
|
|
const [user] = users.get();
|
|
const { team: org } = await user.getOrgMembership();
|
|
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "floating-popup");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "floating-popup",
|
|
basePage: "/event-types",
|
|
});
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "floating-popup",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "floating-popup",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "floating-popup",
|
|
calLink: `${user.username}/multiple-duration`,
|
|
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
|
|
});
|
|
});
|
|
|
|
test("open Embed Dialog and choose element-click for First Event Type", async ({ page, users }) => {
|
|
const [user] = users.get();
|
|
const embedUrl = await clickFirstEventTypeEmbedButton(page);
|
|
const { team: org } = await user.getOrgMembership();
|
|
|
|
await expectToBeNavigatingToEmbedTypesDialog(page, {
|
|
embedUrl,
|
|
basePage: "/event-types",
|
|
});
|
|
|
|
chooseEmbedType(page, "element-click");
|
|
|
|
await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, {
|
|
embedUrl,
|
|
embedType: "element-click",
|
|
basePage: "/event-types",
|
|
});
|
|
await expectToContainValidCode(page, {
|
|
language: "html",
|
|
embedType: "element-click",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
await goToReactCodeTab(page);
|
|
await expectToContainValidCode(page, {
|
|
language: "react",
|
|
embedType: "element-click",
|
|
orgSlug: org.slug,
|
|
});
|
|
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await expectToContainValidPreviewIframe(page, {
|
|
embedType: "element-click",
|
|
calLink: `${user.username}/multiple-duration`,
|
|
bookerUrl: getOrgFullOrigin(org?.slug ?? ""),
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
type EmbedType = "inline" | "floating-popup" | "element-click";
|
|
function chooseEmbedType(page: Page, embedType: EmbedType) {
|
|
page.locator(`[data-testid=${embedType}]`).click();
|
|
}
|
|
|
|
async function goToReactCodeTab(page: Page) {
|
|
// To prevent early timeouts
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(1000);
|
|
await page.locator("[data-testid=horizontal-tab-react]").click();
|
|
}
|
|
|
|
async function clickEmbedButton(page: Page) {
|
|
const embedButton = page.locator("[data-testid=embed]");
|
|
const embedUrl = await embedButton.getAttribute("data-test-embed-url");
|
|
embedButton.click();
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
return embedUrl!;
|
|
}
|
|
|
|
async function clickFirstEventTypeEmbedButton(page: Page) {
|
|
const menu = page.locator("[data-testid*=event-type-options]").first();
|
|
await menu.click();
|
|
const embedUrl = await clickEmbedButton(page);
|
|
return embedUrl;
|
|
}
|
|
|
|
async function expectToBeNavigatingToEmbedTypesDialog(
|
|
page: Page,
|
|
{ embedUrl, basePage }: { embedUrl: string | null; basePage: string }
|
|
) {
|
|
if (!embedUrl) {
|
|
throw new Error("Couldn't find embedUrl");
|
|
}
|
|
await page.waitForURL((url) => {
|
|
return (
|
|
url.pathname === basePage &&
|
|
url.searchParams.get("dialog") === "embed" &&
|
|
url.searchParams.get("embedUrl") === embedUrl
|
|
);
|
|
});
|
|
}
|
|
|
|
async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
|
|
page: Page,
|
|
{
|
|
embedUrl,
|
|
embedType,
|
|
basePage,
|
|
}: {
|
|
embedUrl: string | null;
|
|
embedType: EmbedType;
|
|
basePage: string;
|
|
}
|
|
) {
|
|
if (!embedUrl) {
|
|
throw new Error("Couldn't find embedUrl");
|
|
}
|
|
await page.waitForURL((url) => {
|
|
return (
|
|
url.pathname === basePage &&
|
|
url.searchParams.get("dialog") === "embed" &&
|
|
url.searchParams.get("embedUrl") === embedUrl &&
|
|
url.searchParams.get("embedType") === embedType &&
|
|
url.searchParams.get("embedTabName") === "embed-code"
|
|
);
|
|
});
|
|
}
|
|
|
|
async function expectToContainValidCode(
|
|
page: Page,
|
|
{
|
|
embedType,
|
|
language,
|
|
orgSlug,
|
|
}: { embedType: EmbedType; language: "html" | "react"; orgSlug: string | null }
|
|
) {
|
|
if (language === "react") {
|
|
return expectValidReactEmbedSnippet(page, { embedType, orgSlug });
|
|
}
|
|
if (language === "html") {
|
|
return expectValidHtmlEmbedSnippet(page, { embedType, orgSlug });
|
|
}
|
|
throw new Error("Unknown language");
|
|
}
|
|
|
|
async function expectValidHtmlEmbedSnippet(
|
|
page: Page,
|
|
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
|
|
) {
|
|
const embedCode = await page.locator("[data-testid=embed-code]").inputValue();
|
|
expect(embedCode).toContain("function (C, A, L)");
|
|
expect(embedCode).toContain(`Cal ${embedType} embed code begins`);
|
|
if (orgSlug) {
|
|
expect(embedCode).toContain(orgSlug);
|
|
}
|
|
|
|
// Html/VanillaJS embed needs namespace to call an instruction
|
|
// Verify Cal.ns.abc("ui") or Cal.ns["abc"]("ui")
|
|
expect(embedCode).toMatch(/.*Cal\.ns[^(]+\("ui/);
|
|
|
|
const dom = parse(embedCode);
|
|
const scripts = dom.getElementsByTagName("script");
|
|
assertThatCodeIsValidVanillaJsCode(scripts[0].innerText);
|
|
|
|
return {
|
|
message: () => `passed`,
|
|
pass: true,
|
|
};
|
|
}
|
|
|
|
function assertThatCodeIsValidVanillaJsCode(code: string) {
|
|
const lintResult = linter.verify(code, {
|
|
env: {
|
|
browser: true,
|
|
},
|
|
parserOptions: {
|
|
ecmaVersion: 2021,
|
|
},
|
|
globals: {
|
|
Cal: "readonly",
|
|
},
|
|
rules: eslintRules,
|
|
});
|
|
if (lintResult.length) {
|
|
console.log(
|
|
JSON.stringify({
|
|
lintResult,
|
|
code,
|
|
})
|
|
);
|
|
}
|
|
expect(lintResult.length).toBe(0);
|
|
}
|
|
|
|
function assertThatCodeIsValidReactCode(code: string) {
|
|
const lintResult = linter.verify(code, {
|
|
env: {
|
|
browser: true,
|
|
},
|
|
parserOptions: {
|
|
ecmaVersion: 2021,
|
|
ecmaFeatures: {
|
|
jsx: true,
|
|
},
|
|
sourceType: "module",
|
|
},
|
|
rules: eslintRules,
|
|
});
|
|
if (lintResult.length) {
|
|
console.log(
|
|
JSON.stringify({
|
|
lintResult,
|
|
code,
|
|
})
|
|
);
|
|
}
|
|
expect(lintResult.length).toBe(0);
|
|
}
|
|
|
|
async function expectValidReactEmbedSnippet(
|
|
page: Page,
|
|
{ embedType, orgSlug }: { embedType: EmbedType; orgSlug: string | null }
|
|
) {
|
|
const embedCode = await page.locator("[data-testid=embed-react]").inputValue();
|
|
expect(embedCode).toContain("export default function MyApp(");
|
|
expect(embedCode).toContain(
|
|
embedType === "floating-popup" ? "floatingButton" : embedType === "inline" ? `<Cal` : "data-cal-link"
|
|
);
|
|
// React embed doesn't need to access .ns to call an instruction
|
|
expect(embedCode).toContain('cal("ui"');
|
|
if (orgSlug) {
|
|
expect(embedCode).toContain(orgSlug);
|
|
}
|
|
|
|
assertThatCodeIsValidReactCode(embedCode);
|
|
|
|
return {
|
|
message: () => `passed`,
|
|
pass: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Let's just check if iframe is opened with preview.html. preview.html tests are responsibility of embed-core
|
|
*/
|
|
async function expectToContainValidPreviewIframe(
|
|
page: Page,
|
|
{ embedType, calLink, bookerUrl }: { embedType: EmbedType; calLink: string; bookerUrl?: string }
|
|
) {
|
|
bookerUrl = bookerUrl || `${WEBAPP_URL}`;
|
|
expect(await page.locator("[data-testid=embed-preview]").getAttribute("src")).toContain(
|
|
`/preview.html?embedType=${embedType}&calLink=${calLink}&embedLibUrl=${EMBED_LIB_URL}&bookerUrl=${bookerUrl}`
|
|
);
|
|
}
|