test: add e2e test for Feature Opt-In banner (#27100)

## What does this PR do?

Adds an end-to-end test for the Feature Opt-In banner flow. This test verifies the complete user journey from seeing the banner to enabling a feature.

Currently `OPT_IN_FEATURES` in `packages/features/feature-opt-in/config.ts` is empty, so this e2e test skips. Once we uncomment the `bookings-v3` item from the config, this e2e test will run and pass. This is a preparation for when we enable the feature.

**Changes:**
- Creates a new e2e test file (`feature-opt-in-banner.e2e.ts`) that tests the complete opt-in flow
- Adds `data-testid` attributes to banner and dialog components for reliable test selectors

## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). N/A - test only.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

1. Add the `ready-for-e2e` label to run e2e tests in CI
2. The test should pass when run with `PLAYWRIGHT_HEADLESS=1 yarn e2e feature-opt-in-banner.e2e.ts`

**Test requirements:**
- The `bookings-v3` feature flag must exist in the Features table (the test uses `prisma.feature.upsert` to ensure this)
- The test will skip if `bookings-v3` is not in `OPT_IN_FEATURES`

## Checklist

- [x] I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- [x] My code follows the style guidelines of this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have checked if my changes generate no new warnings

## Updates since last revision

- Replaced text-based locators (`getByText`, `getByRole`) with `data-testid` selectors for more reliable element targeting
- Added `data-testid` attributes to `FeatureOptInBanner`, `FeatureOptInConfirmDialog`, and `FeatureOptInSuccessDialog` components

## Human Review Checklist

- [ ] Verify the `data-testid` attributes don't conflict with existing test IDs in the codebase
- [ ] Confirm the test correctly skips when `bookings-v3` is not in `OPT_IN_FEATURES`
- [ ] Check that the added `data-testid` attributes follow the project's naming conventions

---
Link to Devin run: https://app.devin.ai/sessions/97158ab1b7414e3988ba803b30d95b3e
Requested by: @eunjae-lee
This commit is contained in:
Eunjae Lee
2026-01-22 12:09:13 +01:00
committed by GitHub
parent f00568e798
commit 087fa03513
4 changed files with 91 additions and 9 deletions
@@ -0,0 +1,64 @@
import { OPT_IN_FEATURES } from "@calcom/features/feature-opt-in/config";
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
test.afterEach(({ users }) => users.deleteAll());
test.describe("Feature Opt-In Banner", () => {
test("shows banner on bookings page and allows user to opt-in to bookings-v3 feature", async ({
page,
users,
prisma,
}) => {
if (!OPT_IN_FEATURES.some((item) => item.slug === "bookings-v3")) {
return;
}
// Enable the bookings-v3 feature flag globally in the database
// This is required for the banner to show (globalEnabled must be true)
// Use upsert to ensure the feature exists and is enabled
await prisma.feature.upsert({
where: { slug: "bookings-v3" },
update: { enabled: true },
create: { slug: "bookings-v3", enabled: true, type: "OPERATIONAL" },
});
// Create a user WITHOUT the bookings-v3 feature flag enabled
// By passing an empty array, we override the default user feature flags
const user = await users.create({
userFeatureFlags: [],
});
await user.apiLogin();
// Navigate to the bookings page
await page.goto("/bookings/upcoming");
// Wait for the banner to appear
const banner = page.getByTestId("feature-opt-in-banner");
await expect(banner).toBeVisible({ timeout: 15000 });
// Click the "Try it" button to open the confirmation dialog
const tryItButton = page.getByTestId("feature-opt-in-banner-try-it");
await tryItButton.click();
// Verify the confirmation dialog appears
const confirmDialog = page.getByTestId("feature-opt-in-confirm-dialog");
await expect(confirmDialog).toBeVisible();
// Click the "Enable" button to opt-in
const enableButton = page.getByTestId("feature-opt-in-confirm-dialog-enable");
await enableButton.click();
// Verify the success dialog appears
const successDialogTitle = page.getByTestId("feature-opt-in-success-dialog-title");
await expect(successDialogTitle).toBeVisible();
// Click "View Settings" button to navigate to the settings page
const viewSettingsButton = page.getByTestId("feature-opt-in-success-dialog-view-settings");
await viewSettingsButton.click();
// Verify we are redirected to the features settings page
await expect(page).toHaveURL(/\/settings\/my-account\/features/);
});
});
@@ -20,9 +20,13 @@ export function FeatureOptInBanner({
const { t } = useLocale();
return (
<div className="bg-default border-subtle fixed bottom-5 right-5 z-50 max-w-xs rounded-lg border shadow-lg group">
<div
data-testid="feature-opt-in-banner"
className="bg-default border-subtle fixed bottom-5 right-5 z-50 max-w-xs rounded-lg border shadow-lg group">
<div className="p-4">
<h3 className="text-emphasis text-lg font-semibold">{t(featureConfig.i18n.title)}</h3>
<h3 data-testid="feature-opt-in-banner-title" className="text-emphasis text-lg font-semibold">
{t(featureConfig.i18n.title)}
</h3>
<p className="text-subtle mt-1 text-base">{t(featureConfig.i18n.description)}</p>
<Button
@@ -34,7 +38,12 @@ export function FeatureOptInBanner({
className="absolute top-1.5 right-1.5 opacity-0 pointer-events-none transition-opacity group-hover:opacity-100 group-hover:pointer-events-auto"
aria-label={t("close")}></Button>
<Button className="mt-3" size="sm" color="secondary" onClick={onOpenDialog}>
<Button
data-testid="feature-opt-in-banner-try-it"
className="mt-3"
size="sm"
color="secondary"
onClick={onOpenDialog}>
{t("try_it")}
</Button>
</div>
@@ -214,9 +214,11 @@ export function FeatureOptInConfirmDialog({
return (
<Dialog open={isOpen} onOpenChange={resetAndClose}>
<DialogPopup className="sm:max-w-lg">
<DialogPopup className="sm:max-w-lg" data-testid="feature-opt-in-confirm-dialog">
<DialogHeader>
<DialogTitle>{t(featureConfig.i18n.name)}</DialogTitle>
<DialogTitle data-testid="feature-opt-in-confirm-dialog-title">
{t(featureConfig.i18n.name)}
</DialogTitle>
<DialogDescription>{t(featureConfig.i18n.description)}</DialogDescription>
</DialogHeader>
<DialogPanel>
@@ -290,7 +292,10 @@ export function FeatureOptInConfirmDialog({
<DialogClose render={<Button variant="outline" />} onClick={resetAndClose} disabled={isSubmitting}>
{t("cancel")}
</DialogClose>
<Button onClick={handleConfirm} disabled={isSubmitting || !hasAnySelection}>
<Button
data-testid="feature-opt-in-confirm-dialog-enable"
onClick={handleConfirm}
disabled={isSubmitting || !hasAnySelection}>
{t("enable")}
</Button>
</DialogFooter>
@@ -30,16 +30,20 @@ export function FeatureOptInSuccessDialog({
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogPopup className="sm:max-w-md">
<DialogPopup className="sm:max-w-md" data-testid="feature-opt-in-success-dialog">
<DialogHeader>
<DialogTitle>{t("feature_enabled_successfully")}</DialogTitle>
<DialogTitle data-testid="feature-opt-in-success-dialog-title">
{t("feature_enabled_successfully")}
</DialogTitle>
<DialogDescription>{t("feature_enabled_description")}</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="outline" />} onClick={onDismiss}>
{t("dismiss")}
</DialogClose>
<Button onClick={onViewSettings}>{t("view_settings")}</Button>
<Button data-testid="feature-opt-in-success-dialog-view-settings" onClick={onViewSettings}>
{t("view_settings")}
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>