feat: enable onboarding-v3 globally and update e2e tests (#27922)

* feat: enable onboarding-v3 globally and update e2e tests

- Add migration to set onboarding-v3 feature flag enabled=true
- Rewrite onboarding.e2e.ts for v3 flow (Plan Selection → Personal Settings → Calendar)
- Update URL assertions in signup, team-invitation, org-invitation, ab-tests-redirect, and auth tests to accept both old and new onboarding paths

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* fix: use pathname-only matching in waitForURL to prevent false matches on callbackUrl query param

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* use test IDs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-02-13 10:37:47 +00:00
1 parent 1b6b91b0ad
commit 7aefefc31f
9 files changed
+61 -88

No files matched your search

@@ -151,6 +151,7 @@ export const OnboardingView = ({ userEmail }: OnboardingViewProps) => {
footer={
<div className="flex w-full justify-end gap-2">
<Button
data-testid="onboarding-continue-btn"
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
@@ -117,6 +117,7 @@ export const PersonalCalendarView = ({ userEmail }: PersonalCalendarViewProps) =
{t("onboarding_skip_for_now")}
</Button>
<Button
data-testid="onboarding-continue-btn"
color="primary"
className="rounded-[10px]"
onClick={handleContinue}
+2 -3
View File
@@ -1,5 +1,4 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
@@ -77,12 +76,12 @@ test.describe("apps/ A/B tests", () => {
await expect(locator).toBeDefined();
});
test("should render the /getting-started", async ({ page, users }) => {
test("should render the /onboarding/personal/calendar", async ({ page, users }) => {
const user = await users.create({ completedOnboarding: false, name: null });
await user.apiLogin();
await page.goto("/getting-started/connected-calendar");
await page.goto("/onboarding/personal/calendar");
const locator = page.getByText("Apple Calendar");
+1 -3
View File
@@ -1,5 +1,4 @@
import { expect } from "@playwright/test";
import { test } from "../lib/fixtures";
import { submitAndWaitForResponse } from "../lib/testUtils";
@@ -69,8 +68,7 @@ test.describe("Can signup from a team invite", async () => {
await newPage.fill('input[name="password"]', testUser.password);
await submitAndWaitForResponse(newPage, "/api/auth/signup", { expectedStatusCode: 201 });
// Since it's a new user, it should be redirected to the onboarding
await newPage.locator('text="Welcome to Cal.com!"').waitFor();
await expect(newPage.locator('text="Welcome to Cal.com!"')).toBeVisible();
await newPage.waitForURL(/\/(getting-started|onboarding\/(getting-started|personal\/settings))/);
// We don't need the new browser anymore
await newPage.close();
+19 -62
View File
@@ -1,7 +1,5 @@
import { expect } from "@playwright/test";
import { IdentityProvider } from "@calcom/prisma/enums";
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
@@ -10,80 +8,39 @@ test.afterEach(({ users }) => users.deleteAll());
test.describe("Onboarding", () => {
const testOnboarding = (identityProvider: IdentityProvider) => {
test(`Onboarding Flow - ${identityProvider} user`, async ({ page, users }) => {
test(`Onboarding Flow (v3) - ${identityProvider} user`, async ({ page, users }) => {
const user = await users.create({
completedOnboarding: false,
name: null,
identityProvider,
});
await user.apiLogin();
await page.goto("/getting-started");
// tests whether the user makes it to /getting-started
// after login with completedOnboarding false
await page.waitForURL("/getting-started");
await expect(page.locator('text="Connect your calendar"').first()).toBeVisible(); // Fix race condition
await page.goto("/onboarding/getting-started");
await page.waitForURL("/onboarding/getting-started");
await test.step("step 1 - User Settings", async () => {
const onboarding = page.getByTestId("onboarding");
const form = onboarding.locator("form").first();
const submitButton = form.getByTestId("connect-calendar-button");
await test.step("step 1 - Plan Selection", async () => {
await expect(page.getByTestId("onboarding-continue-btn")).toBeVisible();
await page.getByTestId("onboarding-continue-btn").click();
await page.waitForURL(/.*\/onboarding\/personal\/settings/);
});
// Check required fields
await submitButton.click();
await expect(page.locator("data-testid=required")).toBeVisible();
// happy path
await form.locator("input[name=username]").fill("new user onboarding");
await form.getByLabel("Full name").fill("new user 2");
await form.locator("input[role=combobox]").click();
await page
.locator("*")
.filter({ hasText: /^Europe\/London/ })
.first()
.click();
await submitButton.click();
await expect(page).toHaveURL(/.*connected-calendar/);
await test.step("step 2 - Personal Settings", async () => {
const nameInput = page.locator('input[name="name"]');
await nameInput.fill("new user 2");
await page.locator('button[type="submit"]').click();
await page.waitForURL(/.*\/onboarding\/personal\/calendar/);
const userComplete = await user.self();
expect(userComplete.name).toBe("new user 2");
});
await test.step("step 2 - Connected Calendar", async () => {
const isDisabled = await page.locator("button[data-testid=save-calendar-button]").isDisabled();
await expect(isDisabled).toBe(true);
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*connected-video/);
});
await test.step("step 3 - Connected Video", async () => {
const isDisabled = await page.locator("button[data-testid=save-video-button]").isDisabled();
await expect(isDisabled).toBe(true);
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
});
await test.step("step 4 - Setup Availability", async () => {
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
await expect(isDisabled).toBe(false);
// same here, skip this step.
await page.locator("button[data-testid=save-availability]").click();
await expect(page).toHaveURL(/.*user-profile/);
});
await test.step("step 5- User Profile", async () => {
const onboarding = page.getByTestId("onboarding");
const form = onboarding.locator("form").first();
const submitButton = form.getByRole("button", { name: "Finish setup and get started" });
await submitButton.click();
// should redirect to /event-types after onboarding
await page.waitForURL("/event-types");
await test.step("step 3 - Calendar Connection", async () => {
await expect(page.getByTestId("onboarding-continue-btn")).toBeVisible();
await page.getByTestId("onboarding-continue-btn").click();
await page.waitForURL("/event-types**");
const userComplete = await user.self();
expect(userComplete.bio?.replace("<p><br></p>", "").length).toBe(0);
expect(userComplete.completedOnboarding).toBe(true);
});
});
};
@@ -1,10 +1,7 @@
import prisma from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import type { Browser, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import { moveUserToOrg } from "../lib/orgMigration";
import { bookTeamEvent, doOnOrgDomain, expectPageToBeNotFound, getInviteLink } from "../lib/testUtils";
@@ -505,7 +502,10 @@ async function signupFromInviteLink({
await inviteLinkPage.locator("input[name=email]").fill(email);
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await inviteLinkPage.locator("button[type=submit]").click();
await inviteLinkPage.waitForURL("/getting-started");
await inviteLinkPage.waitForURL((url) => {
const path = url.pathname;
return /\/(getting-started|onboarding\/(getting-started|personal\/settings))/.test(path);
});
return { email };
}
@@ -540,7 +540,10 @@ export async function signupFromEmailInviteLink({
// Check required fields
await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await signupPage.locator("button[type=submit]").click();
await signupPage.waitForURL("/getting-started?from=signup");
await signupPage.waitForURL((url) => {
const path = url.pathname;
return /\/(getting-started|onboarding\/(getting-started|personal\/settings))/.test(path);
});
await context.close();
await signupPage.close();
}
+19 -9
View File
@@ -1,11 +1,10 @@
import { randomBytes } from "node:crypto";
import process from "node:process";
import { APP_NAME, IS_MAILHOG_ENABLED, IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import type { Browser, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { hashSync } from "bcryptjs";
import { randomBytes } from "node:crypto";
import { APP_NAME, IS_PREMIUM_USERNAME_ENABLED, IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { test } from "./lib/fixtures";
import { localize } from "./lib/localize";
import { getEmailsReceivedByUser, getInviteLink } from "./lib/testUtils";
@@ -188,7 +187,10 @@ test.describe("Email Signup Flow Test", async () => {
await page.locator('button[type="submit"]').click();
// Should successfully login with original password
await expect(page).toHaveURL(/\/(getting-started|event-types|teams)/, { timeout: 8000 });
await expect(page).toHaveURL(
/\/(getting-started|onboarding\/getting-started|onboarding\/personal\/settings|event-types|teams)/,
{ timeout: 8000 }
);
// Cleanup
await prisma.verificationToken.deleteMany({ where: { token } });
@@ -393,7 +395,10 @@ test.describe("Email Signup Flow Test", async () => {
// Check required fields
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");
await newPage.waitForURL((url) => {
const path = url.pathname;
return /\/(getting-started|onboarding\/(getting-started|personal\/settings))/.test(path);
});
await newPage.close();
await context.close();
});
@@ -524,7 +529,9 @@ test.describe("Email Signup Flow Test", async () => {
await page.locator('input[name="password"]').fill("Password99!");
await page.getByTestId("signup-submit-button").click();
await expect(page).toHaveURL(/\/getting-started|\/auth\/verify-email/);
await expect(page).toHaveURL(
/\/(getting-started|onboarding\/(getting-started|personal\/settings))|\/auth\/verify-email/
);
const createdUser = await prisma.user.findUnique({
where: { email: userToCreate.email },
@@ -633,6 +640,9 @@ async function signupFromInviteLink({
await inviteLinkPage.locator("input[name=email]").fill(email);
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await inviteLinkPage.locator("button[type=submit]").click();
await inviteLinkPage.waitForURL("/getting-started");
await inviteLinkPage.waitForURL((url) => {
const path = url.pathname;
return /\/(getting-started|onboarding\/(getting-started|personal\/settings))/.test(path);
});
await context.close();
}
@@ -1,9 +1,7 @@
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { expect } from "@playwright/test";
import { test } from "../lib/fixtures";
import { localize } from "../lib/localize";
import { getInviteLink } from "../lib/testUtils";
@@ -65,7 +63,10 @@ test.describe("Team", () => {
// Check required fields
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");
await newPage.waitForURL((url) => {
const path = url.pathname;
return /\/(getting-started|onboarding\/(getting-started|personal\/settings))/.test(path);
});
await newPage.close();
await context.close();
@@ -0,0 +1,3 @@
UPDATE "Feature"
SET "enabled" = true, "updatedAt" = CURRENT_TIMESTAMP
WHERE "slug" = 'onboarding-v3';