+21









Nayan Bagale
GitHub
Tushar Bhatt
Udit Takkar
CarinaWolli
Peer Richelsen
Carina Wollendorfer
Benny Joo
Nizzy
nizzy
Syed Ali Shahbaz
GitHub Actions
Joe Au-Yeung
Hariom Balhara
Hariom Balhara
Eunjae Lee
Lauris Skraucis
Udit Takkar
Morgan
Vijay
supalarry
Alex van Andel
unknown
Bailey Pumfleet
Anik Dhabal Babu
Steven Tey
sean-brydon
amrit
Sanchit Tewari
Rajiv Sahal
Amit Sharma
7bff15cc47
* fix: replace react-hot-toast with sonner for toast notifications
* feat: add country code (#19264)
* perf: Move PlainChat up the hierarchy to remove rerenders (#19255)
* perf: Move PlainChat up the hierarchy to remove rerenders
* rmeove yarn lock
---------
Co-authored-by: Nizzy <nizabizaher@gmail.com>
Co-authored-by: nizzy <140507264+nizzyabi@users.noreply.github.com>
* hotfix (#19266)
* chore: release v4.9.11
* Handle cancelled status (#19272)
* feat: Use new /routing URL for routing forms (#19235)
* feat: Use new /routing URL for routing forms
* Fixes
* Fix bug
* precent crash
---------
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
* fix: remove section on /bookings if no data exists (#19261)
* feat: v2 slots new version (#18758)
* refactor: version old slots
* feat: init new slots endpoints
* chore: range format
* fix: duratin
* fix: duratin
* test: slot releaseAt
* refactor: reserve slot response
* refactor variable name
* docs: have new slots controller in docs
* feat: crud for slots reservations
* refactor: use exclude all for response
* docs
* chore: slots input service
* refactor mini
* refactor: remove unused imports
* docs
* handle orgSlug for dynamic events
* refactor: correct name
* docs
* add optional organizationSlug to BySlug search
* refactor: slot output format
* refactor: return seated slot info
* docs
* rename functions
* refactor: slots seated response
* fix: handle same username in org and non org
* refactor: test format
* fix: allow reservationDuration only for authed requests
* chore: check if auth user has permissions to define custom reservation duration
* refactor: split e2e test files
* refactor: invalid request exception codes
* chore: old slots controller uses all old versions
* refactor: remove depreceated fields (#19279)
* refactor: remove depreceated fields
* chore: use v2 endpoint
* chore: update V2 Api type
* chore
* chore: Refactor to remove `getServerSideProps` for /teams route (#19286)
* remove getServerSideProps
* refactor teams route
* chore: release v4.9.12
* fix: Salesforce - if no contact throw Error (#19271)
* Throw error if no contact is passed to createEvent
* chore: update error message
* chore
---------
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
* fix: missing await router controller (#19276)
* fix: Default people filter in bookings list for admin (#18035)
* default people filter for admin
* chore
* type check
* added test case
* made test concise to reduce flakiness
* add timeout to wait for second get response before counting
* update timeout
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* feat: add CSAT to V2 bookings return (#19278)
* init
* test: v2 bookings returns rating
---------
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
* chore: disable v2 booking seats safe parse error logs (#19292)
* revert: Reapply "fix: Multiple rr-hosts combine to create erroneous availabil… (#19263)
* Reapply "fix: Multiple rr-hosts combine to create erroneous availability (#18772)"
This reverts commit 5dbc6d9ca6.
* Only thing left is a diff to show which slots are removed
* Add logging around time slot results
* Setup logging through axiom
* .... forgot
* fix: type errors
* Only log when there are differences
* Updated from old which had a failing test to new logic
* chore: release v4.9.13
* fix: flaky test (#19294)
* fix: flaky test
* fix another test
* update
---------
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: unknown <adhabal2002@gmail.com>
* chore: calendars controller v2 docs (#19291)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
* Merge branch 'main' into replace-toaster-with-sonner
* add ireland country code (#19117)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
* perf: separate client vs server code from insights layout (#19285)
* add UpgradeTipWrapper
* refactor insights/layout.tsx
---------
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
* feat: Use Folders to organize SMS links on Dub (#19248)
* feat: Use Folders to organize SMS links on Dub
* Update yarn.lock
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
* fix: removing-all-members-toggle-for-fh (#19277)
* fix : removing all members toggle for fh
* ux enhancements
* adding placeholder
* chore: release v5.0.0
* fix flake (#19306)
* chore: migrate `/apps/routing-forms/[...pages]` to App Router (#18956)
* wip
* fix
* fix
* refactor
* rename to routingServerSidePropsConfig
* refactor
* refactor
* add routing-link to useIsBookingPage hook
* remove Head component
* fix
* redirect user to apps/routing-forms/forms if user goes to apps/routing-forms
* refactor
* remove log
* remove client component not needed
* clean up code
* remove unneeded metadata
* clean up further
* clean up further 2
* fix type check
* routing-link does not need shell
* Fix title for Routing Form public link and also remove any
* Remove ; in HTML
* Remove unnecessary page reload
---------
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
* chore: migrate api for team upgrade to App Router (#19100)
* remove old pages router api
* add new app router api
* fix
* fix redirect
* refactor
* fix: cut off and alignment bug (#19317)
* fix: assignment label cut-off
* fix: add all team members label is vertically aligned
* fix: nested scrolls on /bookings (#19284)
* fix: Can't change availability for managed event type (#19006)
* fix: flaky test organization.spec
* fix
* fix: bulk-update-in-scheduleId-in-detailed-view (#19295)
* fix: bulk-update-in-scheduleId-in-detailed-view
* minor-change
---------
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
* feat: prevent automatic query parameters when opening booking page (#18132)
- Resolve the user hindrance where the event's booking page should not have search parameters such as date and month in the initial page load
- Contains changes for the fix of issue #18094 as well as it is needed
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* feat: atoms adjustments with ui v3 styles (#19320)
* refactor: Clean out old code, no longer needed (#19323)
* refactor: Clean out old code, no longer needed
* Removed a sneaky one keen to stay behind
* chore: release v5.0.1
* feat: Create `UserCreationService` and use in API V1 create user endpoint (#19150)
* Create `createUser` method in `userCreationService`
* Refactor `UserRepository` create method to accept prisma input and remove business logic
* API use `UserCreationService`
* Move slugify to service
* Use hashPassword instead
* Type fixes in `UserCreationService`
* Add `userCreationService` tests
* API accept data object
* Type fixes
* Add user _post test
* Add test for locked user
* Add locked param to log
* Add user repository tests
* Do not return locked status
* Explicitly pass `locked` prop
* Fix tests when locked isn't returned
* Fix tests
* Pass locked prop
* Edit test name
* Use logger
* Fix passing hashed password
* fix (#19298)
* fix: timezone input text is now visible in dark mode (#19325)
* feat: make first tab active by default (#19315)
* feat: make first tab active by default
* perf: optimize event types tabs rendering with useMemo
---------
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* perf: Use one PageWrapper instance for all settings pages (#18752)
* add a layoutfile to use-page-wrapper route group
* refactor and move bookings status page to route group
* improve logic in pagesAndRewritePaths
* fix
* add use client directive
* move /403 and /500
* move /auth/error
* move /connect-and-join
* move /enterprise
* move /maintenance
* move /more
* move /upgrade
* chore: remove WithLayout from 8 low-risk single pages (#18745)
* move /403 and /500
* move /auth/error
* move /connect-and-join
* move /enterprise
* move /maintenance
* move /more
* move /upgrade
* move /availability pages
* move /event-types pages
* move /insights pages
* move /apps to use-page-wrapper group
* move /getting-started to use-page-wrapper group
* move /signup to use-page-wrapper group
* move /workflows to use-page-wrapper group
* move /video pages to use-page-wrapper group
* pass dehydratedState in layout
* pass dehydratedState in layout
* move settings/(settings-layout) to use-page-wrapper
* move settings/(admin-layout) to use-page-wrapper
* move remaining settings pages to (use-page-wrapper)
* fix test
* remove unused code
* remove unused files
* fix type check
* remove test tag
* remove page
* RSC should not default export client components
* fix: Can't use unsaved booking question as variable in event name (#18604)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* chore: remove WithLayout from not-found.tsx (#19331)
* fix: redirect logic in /settings/admin/apps (#19332)
* chore: v3 fixed filters, new profile view (#19312)
* fixed filters with badge
* aligned recurrence in bookings
* removed new badge
* new profile view
* fixed try for free button
* fixed form input
* fix: Issues with date changes in column view (#19258)
* fix: coulmn view date change
* Update store.ts
---------
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
* chore: fixed tooltip position for routing form (#19267)
* fixed tooltip position for routing form
* fixed offset
* fix: Fairness when routing (#18550)
* Partial push of new filters
* Let's always return the passed host array when a filter doesn't need to be applied
* fix: isFixed typing in same round robin host check
* Add main bulk of rewritten logic for routed fairness support
* Remove console.log
* Add getAttributeWithEnabledWeights to AttributeRepository
* Add OOO entry repository
* Use getOrderedListOfLuckyUsers in lead threshold filtering
* Shortfall already includes weight
* test against minShortfall
* first attempt to fix fallbacks
* Added testcase updates
* Partially fix filterHosts test
* Some test updates
* Removed email field from normalization
* fix no available users found error
* fix unit test
* remove fallback from same host reschedule return
* fixes and improvements for findQualifiedHosts
* include fallback logic in handleNewBooking
* use fallback in handleNewBooking
* fix for contact owner in handleNewBooking
* WIP - comment
* pass routingFormResponseId
* fix type error
* pass routingFormResponse
* fixes in treshold logic
* remove reason
* merge qualified and fallback availabilities
* fix type error
* add and fix tests
* clean up test file
* fix filterHostsBySameRoundRobinHost tests
* fix findQualifiedHosts.test.ts
* fix getRoutedUsers.test.ts
* fix tests
* fix isRerouting check
* fix start time for two weeks check
* fix fallback hosts
* fix test
* fallback hosts should also include qualified hosts
* fix type error
* fix typos
* add contact owner to fallbacks
* fix typo
* fix qualifiedRRUsers
* set fixed users when no hosts
* add tets + code clean up
* add test for reschedule with same host
* fix typo
* add more tests for findQualifiedHosts
* remove skip from test
* add comment
* remove unused code
* fix fallback users in loadAndValidateUsers
* improve naming for fallback
* rename missed fallback variable
* fix reschedule with same rr host in handleNewBooking
* fixes for fallback slots
* add more tests + small fixes for contact owner
* fix tests and type errors
* add logs for fallback
* add type:json to logger
* Fix small merge issue
* fix end time if two weeks are cut off
* add more tests for contact owner availability check
* add new logic for two weeks fallback
* fix reschedule same RR host test
---------
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
* fix: EditLocationDialog footer positioning and structure (#19335)
* fix: EditLocationDialog footer positioning and structure
* fix: make RescheduleDialog footer layout and button structure right
* fix: input dialogue glitch now works great
* made tests happy
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* Merge branch 'main' into replace-toaster-with-sonner
* chore: remove DefaultToast from toast.stories.mdx
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Nizzy <nizabizaher@gmail.com>
Co-authored-by: nizzy <140507264+nizzyabi@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: GitHub Actions <github-actions@github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Sanchit Tewari <46245135+sanchitttt@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
577 lines
21 KiB
TypeScript
577 lines
21 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
import { JSDOM } from "jsdom";
|
|
import type { Messages } from "mailhog";
|
|
import path from "path";
|
|
import { uuid } from "short-uuid";
|
|
|
|
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
|
|
|
import type { createEmailsFixture } from "../fixtures/emails";
|
|
import { test } from "../lib/fixtures";
|
|
import { fillStripeTestCheckout } from "../lib/testUtils";
|
|
import { getEmailsReceivedByUser } from "../lib/testUtils";
|
|
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";
|
|
|
|
async function expectEmailWithSubject(
|
|
page: Page,
|
|
emails: ReturnType<typeof createEmailsFixture>,
|
|
userEmail: string,
|
|
subject: string
|
|
) {
|
|
if (!emails) return null;
|
|
|
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
await page.waitForTimeout(2000);
|
|
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
|
|
|
|
const allEmails = (receivedEmails as Messages).items;
|
|
const email = allEmails.find((email) => email.subject === subject);
|
|
if (!email) {
|
|
throw new Error(`Email with subject ${subject} not found`);
|
|
}
|
|
const dom = new JSDOM(email.html);
|
|
return dom;
|
|
}
|
|
|
|
export async function expectOrganizationCreationEmailToBeSent({
|
|
page,
|
|
emails,
|
|
userEmail,
|
|
orgSlug,
|
|
}: {
|
|
page: Page;
|
|
emails: ReturnType<typeof createEmailsFixture>;
|
|
userEmail: string;
|
|
orgSlug: string;
|
|
}) {
|
|
const dom = await expectEmailWithSubject(page, emails, userEmail, "Your organization has been created");
|
|
const document = dom?.window?.document;
|
|
expect(document?.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
|
|
return dom;
|
|
}
|
|
|
|
async function expectOrganizationCreationEmailToBeSentWithLinks({
|
|
page,
|
|
emails,
|
|
userEmail,
|
|
oldUsername,
|
|
newUsername,
|
|
orgSlug,
|
|
}: {
|
|
page: Page;
|
|
emails: ReturnType<typeof createEmailsFixture>;
|
|
userEmail: string;
|
|
oldUsername: string;
|
|
newUsername: string;
|
|
orgSlug: string;
|
|
}) {
|
|
const dom = await expectOrganizationCreationEmailToBeSent({
|
|
page,
|
|
emails,
|
|
userEmail,
|
|
orgSlug,
|
|
});
|
|
const document = dom?.window.document;
|
|
const links = document?.querySelectorAll(`[data-testid="organization-link-info"] [href]`);
|
|
if (!links) {
|
|
throw new Error(`data-testid="organization-link-info doesn't have links`);
|
|
}
|
|
expect((links[0] as unknown as HTMLAnchorElement).href).toContain(oldUsername);
|
|
expect((links[1] as unknown as HTMLAnchorElement).href).toContain(newUsername);
|
|
}
|
|
|
|
export async function expectEmailVerificationEmailToBeSent(
|
|
page: Page,
|
|
emails: ReturnType<typeof createEmailsFixture>,
|
|
userEmail: string
|
|
) {
|
|
const subject = "Cal.com: Verify your account";
|
|
return expectEmailWithSubject(page, emails, userEmail, subject);
|
|
}
|
|
|
|
test.afterEach(({ users, orgs }) => {
|
|
users.deleteAll();
|
|
orgs.deleteAll();
|
|
});
|
|
|
|
function capitalize(text: string) {
|
|
if (!text) {
|
|
return text;
|
|
}
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
}
|
|
|
|
test.describe("Organization", () => {
|
|
test("Admin should be able to create an org where an existing user is made an owner", async ({
|
|
page,
|
|
users,
|
|
emails,
|
|
}) => {
|
|
const appLevelAdmin = await users.create({
|
|
role: "ADMIN",
|
|
});
|
|
await appLevelAdmin.apiLogin();
|
|
|
|
const orgOwnerUsernamePrefix = "owner";
|
|
|
|
const orgOwnerEmail = users.trackEmail({
|
|
username: orgOwnerUsernamePrefix,
|
|
domain: `example.com`,
|
|
});
|
|
|
|
const orgOwnerUser = await users.create({
|
|
username: orgOwnerUsernamePrefix,
|
|
email: orgOwnerEmail,
|
|
role: "ADMIN",
|
|
});
|
|
|
|
const orgOwnerUsernameOutsideOrg = orgOwnerUser.username;
|
|
const orgOwnerUsernameInOrg = orgOwnerEmail.split("@")[0];
|
|
const orgName = capitalize(`${orgOwnerUser.username}`);
|
|
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
|
|
await page.goto("/settings/organizations/new");
|
|
|
|
await test.step("Basic info", async () => {
|
|
// Check required fields
|
|
await page.locator("button[type=submit]").click();
|
|
await expect(page.getByTestId("field-error")).toHaveCount(3);
|
|
|
|
// Happy path
|
|
await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
|
|
});
|
|
|
|
await expectOrganizationCreationEmailToBeSentWithLinks({
|
|
page,
|
|
emails,
|
|
userEmail: orgOwnerEmail,
|
|
oldUsername: orgOwnerUsernameOutsideOrg || "",
|
|
newUsername: orgOwnerUsernameInOrg,
|
|
orgSlug,
|
|
});
|
|
|
|
await test.step("About the organization", async () => {
|
|
// Choosing an avatar
|
|
await page.getByTestId("open-upload-avatar-dialog").click();
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
await page.getByText("Choose a file...").click();
|
|
const fileChooser = await fileChooserPromise;
|
|
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
|
await page.getByTestId("upload-avatar").click();
|
|
|
|
// About text
|
|
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
|
await page.locator("button[type=submit]").click();
|
|
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/onboard-members");
|
|
});
|
|
|
|
await test.step("On-board administrators", async () => {
|
|
await page.waitForSelector('[data-testid="pending-member-list"]');
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
|
|
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
|
|
|
|
//can add members
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
|
|
await page.getByTestId("invite-new-member-button").click();
|
|
await expect(page.getByTestId("pending-member-item").filter({ hasText: adminEmail })).toBeVisible();
|
|
// TODO: Check if invited admin received the invitation email
|
|
// await expectInvitationEmailToBeReceived(
|
|
// page,
|
|
// emails,
|
|
// adminEmail,
|
|
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
|
// );
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
|
|
// can remove members
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
|
await lastRemoveMemberButton.click();
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
await page.getByTestId("publish-button").click();
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/add-teams");
|
|
});
|
|
|
|
await test.step("Create teams", async () => {
|
|
// Filling one team
|
|
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
|
|
|
// Adding another team
|
|
await page.getByTestId("add_a_team").click();
|
|
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
|
|
|
// Finishing the creation wizard
|
|
await page.getByTestId("continue_or_checkout").click();
|
|
await page.waitForURL("/event-types");
|
|
});
|
|
|
|
await test.step("Login as org owner and pay", async () => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
|
|
|
|
await orgOwnerUser.apiLogin();
|
|
await page.goto("/event-types");
|
|
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButton).toBeVisible();
|
|
await upgradeButton.click();
|
|
// Check that stripe checkout is present
|
|
const expectedUrl = "https://checkout.stripe.com";
|
|
|
|
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
|
|
const url = page.url();
|
|
|
|
// Check that the URL matches the expected URL
|
|
expect(url).toContain(expectedUrl);
|
|
|
|
await fillStripeTestCheckout(page);
|
|
|
|
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButtonHidden).toBeHidden();
|
|
});
|
|
|
|
// Verify that the owner's old username redirect is properly set
|
|
await gotoPathAndExpectRedirectToOrgDomain({
|
|
page,
|
|
org: {
|
|
slug: orgSlug,
|
|
},
|
|
path: `/${orgOwnerUsernameOutsideOrg}`,
|
|
expectedPath: `/${orgOwnerUsernameInOrg}`,
|
|
});
|
|
});
|
|
|
|
test("Admin should be able to create an org where the owner doesn't exist yet", async ({
|
|
page,
|
|
users,
|
|
emails,
|
|
}) => {
|
|
const appLevelAdmin = await users.create({
|
|
role: "ADMIN",
|
|
});
|
|
await appLevelAdmin.apiLogin();
|
|
const orgOwnerUsername = `owner`;
|
|
const orgName = capitalize(`${orgOwnerUsername}`);
|
|
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
|
|
const orgOwnerEmail = users.trackEmail({
|
|
username: orgOwnerUsername,
|
|
domain: `example.com`,
|
|
});
|
|
|
|
await page.goto("/settings/organizations/new");
|
|
|
|
await test.step("Basic info", async () => {
|
|
// Check required fields
|
|
await page.locator("button[type=submit]").click();
|
|
await expect(page.getByTestId("field-error")).toHaveCount(3);
|
|
|
|
// Happy path
|
|
await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
|
|
});
|
|
|
|
const dom = await expectOrganizationCreationEmailToBeSent({
|
|
page,
|
|
emails,
|
|
userEmail: orgOwnerEmail,
|
|
orgSlug,
|
|
});
|
|
expect(dom?.window.document.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
|
|
await expectEmailVerificationEmailToBeSent(page, emails, orgOwnerEmail);
|
|
// Rest of the steps remain same as org creation with existing user as owner. So skipping them
|
|
});
|
|
|
|
test("User can create and upgrade a org", async ({ page, users, emails }) => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
|
const stringUUID = uuid();
|
|
|
|
const orgOwnerUsername = `owner-${stringUUID}`;
|
|
|
|
const targetOrgEmail = users.trackEmail({
|
|
username: orgOwnerUsername,
|
|
domain: `example.com`,
|
|
});
|
|
const orgOwnerUser = await users.create({
|
|
username: orgOwnerUsername,
|
|
email: targetOrgEmail,
|
|
});
|
|
|
|
await orgOwnerUser.apiLogin();
|
|
const orgName = capitalize(`${orgOwnerUsername}`);
|
|
await page.goto("/settings/organizations/new");
|
|
|
|
await test.step("Basic info", async () => {
|
|
// These values are infered due to an existing user being signed
|
|
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
|
|
expect(await page.locator("input[name=slug]").inputValue()).toBe("example");
|
|
|
|
await page.locator("input[name=name]").fill(orgName);
|
|
await page.locator("input[name=slug]").fill(orgOwnerUsername);
|
|
|
|
await page.locator("button[type=submit]").click();
|
|
});
|
|
|
|
await test.step("About the organization", async () => {
|
|
// Choosing an avatar
|
|
await page.getByTestId("open-upload-avatar-dialog").click();
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
await page.getByText("Choose a file...").click();
|
|
const fileChooser = await fileChooserPromise;
|
|
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
|
await page.getByTestId("upload-avatar").click();
|
|
|
|
// About text
|
|
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
|
await page.locator("button[type=submit]").click();
|
|
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/onboard-members");
|
|
});
|
|
|
|
await test.step("On-board administrators", async () => {
|
|
await page.waitForSelector('[data-testid="pending-member-list"]');
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
|
|
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
|
|
|
|
//can add members
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
|
|
await page.getByTestId("invite-new-member-button").click();
|
|
await expect(page.getByTestId("pending-member-item").filter({ hasText: adminEmail })).toBeVisible();
|
|
// TODO: Check if invited admin received the invitation email
|
|
// await expectInvitationEmailToBeReceived(
|
|
// page,
|
|
// emails,
|
|
// adminEmail,
|
|
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
|
// );
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
|
|
// can remove members
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
|
await lastRemoveMemberButton.click();
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
await page.getByTestId("publish-button").click();
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/add-teams");
|
|
});
|
|
|
|
await test.step("Create teams", async () => {
|
|
// Filling one team
|
|
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
|
|
|
// Adding another team
|
|
await page.getByTestId("add_a_team").click();
|
|
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
|
|
|
// Finishing the creation wizard
|
|
await page.getByTestId("continue_or_checkout").click();
|
|
});
|
|
|
|
await test.step("Login as org owner and pay", async () => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
|
|
await orgOwnerUser.apiLogin();
|
|
await page.goto("/event-types");
|
|
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButton).toBeVisible();
|
|
await upgradeButton.click();
|
|
// Check that stripe checkout is present
|
|
const expectedUrl = "https://checkout.stripe.com";
|
|
|
|
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
|
|
const url = page.url();
|
|
|
|
// Check that the URL matches the expected URL
|
|
expect(url).toContain(expectedUrl);
|
|
|
|
await fillStripeTestCheckout(page);
|
|
|
|
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButtonHidden).toBeHidden();
|
|
});
|
|
});
|
|
|
|
test("User gets prompted with >=3 teams to upgrade & can transfer existing teams to org", async ({
|
|
page,
|
|
users,
|
|
}) => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
|
|
const numberOfTeams = 3;
|
|
const stringUUID = uuid();
|
|
|
|
const orgOwnerUsername = `owner-${stringUUID}`;
|
|
|
|
const targetOrgEmail = users.trackEmail({
|
|
username: orgOwnerUsername,
|
|
domain: `example.com`,
|
|
});
|
|
const orgOwnerUser = await users.create(
|
|
{
|
|
username: orgOwnerUsername,
|
|
email: targetOrgEmail,
|
|
},
|
|
{ hasTeam: true, numberOfTeams }
|
|
);
|
|
|
|
await orgOwnerUser.apiLogin();
|
|
|
|
await page.goto("/teams");
|
|
|
|
await test.step("Has org self serve banner", async () => {
|
|
// These values are infered due to an existing user being signed
|
|
const selfServeButtonLocator = await page.getByTestId("setup_your_org_action_button");
|
|
await expect(selfServeButtonLocator).toBeVisible();
|
|
|
|
await selfServeButtonLocator.click();
|
|
await page.waitForURL("/settings/organizations/new");
|
|
});
|
|
|
|
await test.step("Basic info", async () => {
|
|
// These values are infered due to an existing user being signed
|
|
const slugLocator = await page.locator("input[name=slug]");
|
|
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
|
|
expect(await slugLocator.inputValue()).toBe("example");
|
|
|
|
await slugLocator.fill(`example-${stringUUID}`);
|
|
|
|
await page.locator("button[type=submit]").click();
|
|
});
|
|
|
|
await test.step("About the organization", async () => {
|
|
// Choosing an avatar
|
|
await page.getByTestId("open-upload-avatar-dialog").click();
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
|
await page.getByText("Choose a file...").click();
|
|
const fileChooser = await fileChooserPromise;
|
|
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
|
|
await page.getByTestId("upload-avatar").click();
|
|
|
|
// About text
|
|
await page.locator('textarea[name="about"]').fill("This is a testing org");
|
|
await page.locator("button[type=submit]").click();
|
|
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/onboard-members");
|
|
});
|
|
|
|
await test.step("On-board administrators", async () => {
|
|
await page.waitForSelector('[data-testid="pending-member-list"]');
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
|
|
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
|
|
|
|
//can add members
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
|
|
await page.getByTestId("invite-new-member-button").click();
|
|
await expect(page.getByTestId("pending-member-item").filter({ hasText: adminEmail })).toBeVisible();
|
|
// TODO: Check if invited admin received the invitation email
|
|
// await expectInvitationEmailToBeReceived(
|
|
// page,
|
|
// emails,
|
|
// adminEmail,
|
|
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
|
|
// );
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
|
|
// can remove members
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
|
|
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
|
|
await lastRemoveMemberButton.click();
|
|
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
|
|
await page.getByTestId("publish-button").click();
|
|
// Waiting to be in next step URL
|
|
await page.waitForURL("/settings/organizations/*/add-teams");
|
|
});
|
|
|
|
await test.step("Move existing teams to org", async () => {
|
|
// No easy way to get all team checkboxes so we fill all checkboxes on the page in
|
|
const foundCheckboxes = page.locator('input[type="checkbox"]');
|
|
const count = await foundCheckboxes.count();
|
|
for (let i = 0; i < count; i++) {
|
|
const checkbox = foundCheckboxes.nth(i);
|
|
await checkbox.click();
|
|
}
|
|
});
|
|
|
|
await test.step("Create teams", async () => {
|
|
// Filling one team
|
|
await page.locator('input[name="teams.0.name"]').fill("Marketing");
|
|
|
|
// Adding another team
|
|
await page.getByTestId("new-team-dialog-button").click();
|
|
await page.locator('input[name="teams.1.name"]').fill("Sales");
|
|
|
|
// Finishing the creation wizard
|
|
await page.getByTestId("continue_or_checkout").click();
|
|
});
|
|
|
|
await test.step("Login as org owner and pay", async () => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
|
|
await orgOwnerUser.apiLogin();
|
|
await page.goto("/event-types");
|
|
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButton).toBeVisible();
|
|
await upgradeButton.click();
|
|
// Check that stripe checkout is present
|
|
const expectedUrl = "https://checkout.stripe.com";
|
|
|
|
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
|
|
const url = page.url();
|
|
|
|
// Check that the URL matches the expected URL
|
|
expect(url).toContain(expectedUrl);
|
|
|
|
await fillStripeTestCheckout(page);
|
|
|
|
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
|
|
|
|
await expect(upgradeButtonHidden).toBeHidden();
|
|
});
|
|
|
|
await test.step("Ensure correctnumberOfTeams are migrated", async () => {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
await page.goto("/teams");
|
|
const teamListItems = await page.getByTestId("team-list-item-link").all();
|
|
|
|
// Number of teams migrated + the two created in the create teams step
|
|
expect(teamListItems.length).toBe(numberOfTeams + 2);
|
|
});
|
|
});
|
|
});
|
|
|
|
async function fillAndSubmitFirstStepAsAdmin(
|
|
page: Page,
|
|
targetOrgEmail: string,
|
|
orgName: string,
|
|
orgSlug: string
|
|
) {
|
|
await page.locator("input[name=orgOwnerEmail]").fill(targetOrgEmail);
|
|
// Since we are admin fill in this infomation instead of deriving it
|
|
await page.locator("input[name=name]").fill(orgName);
|
|
await page.locator("input[name=slug]").fill(orgSlug);
|
|
|
|
// Fill in seat infomation
|
|
await page.locator("input[name=seats]").fill("30");
|
|
await page.locator("input[name=pricePerSeat]").fill("30");
|
|
|
|
await Promise.all([
|
|
page.waitForResponse("**/api/trpc/organizations/create**"),
|
|
page.locator("button[type=submit]").click(),
|
|
]);
|
|
}
|