From 8ef7b56b37d016d3e5dcfb9b6ed3316f3cc7b0a7 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 20:42:12 -0300 Subject: [PATCH 01/10] feat: re-trigger pr.yml when ready-for-e2e label is added (#26376) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/run-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-ci.yml b/.github/workflows/run-ci.yml index a82c106d362cfe..452de59ab5e111 100644 --- a/.github/workflows/run-ci.yml +++ b/.github/workflows/run-ci.yml @@ -11,7 +11,7 @@ permissions: jobs: trigger: name: Trigger CI - if: github.event.label.name == 'run-ci' + if: github.event.label.name == 'run-ci' || github.event.label.name == 'ready-for-e2e' runs-on: ubuntu-latest steps: - name: Verify and trigger CI From 64dcf5d3f6b234a373e7ca9da1d34409854893ac Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 21:10:55 -0300 Subject: [PATCH 02/10] fix: add cleanup and mock embed-iframe to prevent test teardown leak (#26377) * fix: add cleanup and mock embed-iframe to prevent test teardown leak The CancelBooking.cancellationFee.test.tsx was causing an unhandled jsdom exception during test teardown due to the @calcom/embed-core/embed-iframe module scheduling timers that would fire after the jsdom environment was destroyed. Changes: - Mock @calcom/embed-core/embed-iframe to prevent sdkActionManager from scheduling timers during tests - Add afterEach cleanup to ensure React Testing Library properly cleans up between tests - Remove unused React import Co-Authored-By: keith@cal.com * fix: add afterAll cleanup to restore scrollIntoView and unmock embed-iframe Add proper cleanup in afterAll to: - Restore Element.prototype.scrollIntoView to its original value - Call vi.unmock for embed-iframe to avoid polluting other tests in the same worker This prevents cross-test pollution that was causing flaky 'Closing rpc while fetch was pending' errors in other test files running in the same Vitest worker. Co-Authored-By: keith@cal.com * fix: add cleanup to TestFormDialog and defer imports in editLocation.handler tests - TestFormDialog.test.tsx: Add fake timers and flush pending timers before cleanup to prevent Radix FocusScope setTimeout from firing after jsdom teardown - editLocation.handler.test.ts: Remove top-level imports to prevent watchlist module loading during test collection (tests are already skipped) Co-Authored-By: keith@cal.com * fix: defer imports in confirm.handler.test.ts to prevent Salesforce GraphQL module loading Tests are already skipped, so imports are not needed during collection phase. This prevents 'Closing rpc while fetch was pending' errors from Salesforce GraphQL module imports. Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../routing-forms/TestFormDialog.test.tsx | 13 ++++++-- .../CancelBooking.cancellationFee.test.tsx | 31 +++++++++++++++++-- .../viewer/bookings/confirm.handler.test.ts | 8 ++--- .../bookings/editLocation.handler.test.ts | 19 ++---------- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx b/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx index 5f2b366b83e0f7..1dbeec8d5bada4 100644 --- a/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx +++ b/apps/web/components/apps/routing-forms/TestFormDialog.test.tsx @@ -1,6 +1,6 @@ -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import type { Mock } from "vitest"; -import { vi } from "vitest"; +import { vi, beforeEach, afterEach, describe, expect, it } from "vitest"; import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute"; @@ -194,6 +194,15 @@ describe("TestFormDialog", () => { beforeEach(() => { resetFindTeamMembersMatchingAttributeLogicResponse(); vi.clearAllMocks(); + vi.useFakeTimers(); + }); + + afterEach(() => { + // Flush any pending timers (like Radix FocusScope setTimeout) before cleanup + // to prevent them from firing after jsdom teardown + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + cleanup(); }); it("renders the dialog when open", () => { diff --git a/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx b/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx index d61226b683795d..ce37d66d7768da 100644 --- a/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx +++ b/apps/web/components/booking/__tests__/CancelBooking.cancellationFee.test.tsx @@ -1,15 +1,40 @@ -import { render, screen } from "@testing-library/react"; -import * as React from "react"; -import { describe, expect, it, vi, beforeAll } from "vitest"; +import { render, screen, cleanup } from "@testing-library/react"; +import { describe, expect, it, vi, beforeAll, afterAll, afterEach } from "vitest"; import * as shouldChargeModule from "@calcom/features/bookings/lib/payment/shouldChargeNoShowCancellationFee"; import CancelBooking from "../CancelBooking"; +// Mock the embed-iframe module to prevent it from scheduling timers/RAF that can cause +// teardown issues when jsdom environment is destroyed +vi.mock("@calcom/embed-core/embed-iframe", () => ({ + sdkActionManager: null, +})); + +// Store original scrollIntoView to restore later +const originalScrollIntoView = Element.prototype.scrollIntoView; + beforeAll(() => { + // jsdom doesn't implement scrollIntoView, so we need to mock it Element.prototype.scrollIntoView = vi.fn(); }); +afterAll(() => { + // Restore scrollIntoView to avoid polluting other tests in the same worker + if (originalScrollIntoView) { + Element.prototype.scrollIntoView = originalScrollIntoView; + } else { + // If it was originally undefined, delete it + delete (Element.prototype as { scrollIntoView?: unknown }).scrollIntoView; + } + // Clean up module mocks to avoid polluting other tests + vi.unmock("@calcom/embed-core/embed-iframe"); +}); + +afterEach(() => { + cleanup(); +}); + vi.mock("@calcom/trpc/react", () => ({ trpc: { viewer: { diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts index f7da466cfe78d6..62a9444f3f1ab4 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.test.ts @@ -1,13 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck // TODO: Bring this test back with the correct setup (no illegal imports) +// NOTE: All imports except vitest are deferred to inside the skipped describe blocks +// to prevent module loading side effects during test collection (which can cause +// "Closing rpc while fetch was pending" errors from Salesforce GraphQL module imports) import { describe, beforeEach, vi, expect, test } from "vitest"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import type { TrpcSessionUser } from "../../../types"; -import { confirmHandler } from "./confirm.handler"; - //eslint-disable-next-line playwright/no-skipped-test describe.skip("confirmHandler", () => { beforeEach(() => { diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts index d0b53781c92ad7..d33680ce06c39f 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.test.ts @@ -1,24 +1,11 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck // TODO: Bring this test back with the correct setup (no illegal imports) +// NOTE: All imports except vitest are deferred to inside the skipped describe blocks +// to prevent module loading side effects during test collection (which can cause +// "Closing rpc while fetch was pending" errors from watchlist module imports) import { describe, expect, test, vi, beforeEach } from "vitest"; -import { prisma } from "@calcom/prisma"; -import { BookingStatus } from "@calcom/prisma/enums"; - -import { - editLocationHandler, - getLocationForOrganizerDefaultConferencingAppInEvtFormat, - SystemError, - UserError, -} from "./editLocation.handler"; - -vi.mock("@calcom/prisma", () => { - return { - prisma: vi.fn(), - }; -}); - describe.skip("getLocationForOrganizerDefaultConferencingAppInEvtFormat", () => { const mockTranslate = vi.fn((key: string) => key); From 94f18f6f9c1753f5e04b542dd18872532a3a52be Mon Sep 17 00:00:00 2001 From: Volnei Munhoz Date: Thu, 1 Jan 2026 21:13:12 -0300 Subject: [PATCH 03/10] chore: disable lint staged from merge main (#26380) --- .husky/pre-commit | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index bbf4dec8aba98c..1d4d20d67fa860 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,7 @@ -yarn lint-staged +if [ -f .git/MERGE_HEAD ]; then + echo "Merge detected. Skipping lint-staged and generators." + exit 0 +fi +yarn lint-staged yarn app-store:build && git add packages/app-store/*.generated.* From b66797a3af07a10668a8d15e531da756e5231488 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 21:21:04 -0300 Subject: [PATCH 04/10] fix: remove PR/commit-specific keys from docs-build cache (#26379) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/docs-build.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index abecee2f75d552..ccf44ed8a6ac46 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -20,16 +20,10 @@ jobs: cache-name: docs-build key-1: ${{ hashFiles('yarn.lock') }} key-2: ${{ hashFiles('docs/**.*', '!**/node_modules') }} - key-3: ${{ github.event.pull_request.number || github.ref }} - key-4: ${{ github.sha }} with: path: | **/docs/** - key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - # Log cache hit - - name: Log Cache Hit - if: steps.cache-docs-build.outputs.cache-hit == 'true' - run: echo "Cache hit for Docs build. Skipping build." + key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - name: Run build if: steps.cache-docs-build.outputs.cache-hit != 'true' working-directory: docs From 919827707f73c2861a70a03f39f5b64d89328305 Mon Sep 17 00:00:00 2001 From: Beto <43630417+betomoedano@users.noreply.github.com> Date: Thu, 1 Jan 2026 18:40:17 -0600 Subject: [PATCH 05/10] feat: add .easignore file to manage EAS build uploads for the companion app (#26375) - Introduced a new .easignore file to specify which files and directories to ignore during EAS builds. - Configured to only upload the companion app folder while excluding unnecessary build artifacts and dependencies. --- .easignore | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .easignore diff --git a/.easignore b/.easignore new file mode 100644 index 00000000000000..fd9cae677a52ea --- /dev/null +++ b/.easignore @@ -0,0 +1,57 @@ +# EAS Build ignore file for monorepo +# Only upload the companion app folder + +# =========================================== +# IGNORE EVERYTHING AT ROOT LEVEL +# =========================================== +/* + +# =========================================== +# EXCEPT THE COMPANION APP +# =========================================== +!companion + +# =========================================== +# BUT IGNORE THESE INSIDE COMPANION +# =========================================== + +# Native folders - EAS generates these during builds (~2.1 GB) +companion/ios +companion/android + +# Dependencies - EAS installs these during builds (~1.6 GB) +companion/node_modules + +# Expo build cache +companion/.expo +companion/dist +companion/web-build + +# Metro +companion/.metro-health-check* + +# WXT Chrome Extension outputs (not needed for mobile builds) +companion/.output +companion/.wxt +companion/dev + +# Kotlin cache +companion/.kotlin + +# Build artifacts +companion/*.jks +companion/*.p8 +companion/*.p12 +companion/*.key +companion/*.mobileprovision + +# Debug logs +companion/*.log + +# TypeScript build info +companion/*.tsbuildinfo + +# Environment files +companion/.env +companion/.env.* +!companion/.env.example From af110633c1fcbc84b9ff31d7cdcdbfb51244b698 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 23:00:56 -0300 Subject: [PATCH 06/10] refactor: detach yarn prisma generate from yarn-install action (#26382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: detach yarn prisma generate from yarn-install action Co-Authored-By: keith@cal.com * fix: add run-prisma-generate input for backward compatibility The yarn-install action now has a run-prisma-generate input that defaults to true for backward compatibility. This ensures CI works correctly since workflow files are pulled from the base branch (main) while actions are pulled from the PR branch. Workflows that have explicit yarn prisma generate steps now set run-prisma-generate: false to avoid running it twice after merge. Co-Authored-By: keith@cal.com * refactor: completely remove prisma generate from yarn-install action Remove all prisma-related code from the yarn-install action: - Remove the run-prisma-generate input parameter - Remove the Generate Prisma client step Remove explicit yarn prisma generate steps from all workflow files. Prisma generation is now handled by the postinstall script in package.json which runs 'turbo run post-install' after yarn install. This triggers @calcom/prisma#post-install which runs 'prisma generate && prisma format'. This makes the yarn-install action have no knowledge of Prisma at all, as requested. Co-Authored-By: keith@cal.com * fix: add generic post-install step to ensure generated files are up-to-date When all caches are hit, yarn install completes quickly without running the postinstall script. This means generated files (like Prisma types) may not be created. Add a generic 'turbo run post-install' step that runs after yarn install to ensure all post-install tasks complete regardless of cache state. This keeps the action from having Prisma-specific knowledge while ensuring the post-install pipeline runs. Co-Authored-By: keith@cal.com * refactor: remove post-install step from yarn-install action Remove the turbo run post-install step as requested. The yarn-install action now only handles yarn install with caching, with no knowledge of post-install tasks or Prisma generation. Let CI show what fails without explicit post-install handling. Co-Authored-By: keith@cal.com * Add back Prisma schema loaded from schema.prisma ✔ Generated Prisma Client (6.16.1) to ./generated/prisma in 442ms ✔ Generated Zod Prisma Types to ./zod in 1.43s ✔ Generated Kysely types (2.2.0) to ./../kysely in 271ms ✔ Generated Prisma Enum Generator to ./enums/index.ts in 176ms where needed * Adding Prisma schema loaded from schema.prisma ✔ Generated Prisma Client (6.16.1) to ./generated/prisma in 451ms ✔ Generated Zod Prisma Types to ./zod in 1.40s ✔ Generated Kysely types (2.2.0) to ./../kysely in 271ms ✔ Generated Prisma Enum Generator to ./enums/index.ts in 205ms where needed for E2E --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/actions/yarn-install/action.yml | 4 +--- .github/workflows/api-v2-unit-tests.yml | 1 + .github/workflows/check-api-v2-breaking-changes.yml | 4 +++- .github/workflows/e2e-app-store.yml | 1 + .github/workflows/e2e-embed-react.yml | 1 + .github/workflows/e2e-embed.yml | 1 + .github/workflows/e2e.yml | 1 + .github/workflows/integration-tests.yml | 1 + .github/workflows/setup-db.yml | 2 ++ .github/workflows/unit-tests.yml | 1 + 10 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/actions/yarn-install/action.yml b/.github/actions/yarn-install/action.yml index a930ce326d37ef..e30334a1c94efe 100644 --- a/.github/actions/yarn-install/action.yml +++ b/.github/actions/yarn-install/action.yml @@ -112,9 +112,7 @@ runs: - name: Install dependencies if: ${{ inputs.skip-install-if-cache-hit != 'true' || steps.all-caches-check.outputs.all-hit != 'true' }} shell: bash - run: | - yarn install --inline-builds - yarn prisma generate + run: yarn install --inline-builds env: # CI optimizations. Overrides yarnrc.yml options (or their defaults) in the CI action. YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps diff --git a/.github/workflows/api-v2-unit-tests.yml b/.github/workflows/api-v2-unit-tests.yml index 20d599ccd83101..5c6fd7b69c4e5b 100644 --- a/.github/workflows/api-v2-unit-tests.yml +++ b/.github/workflows/api-v2-unit-tests.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - name: Run API v2 unit tests working-directory: apps/api/v2 run: | diff --git a/.github/workflows/check-api-v2-breaking-changes.yml b/.github/workflows/check-api-v2-breaking-changes.yml index cc124fc538c9f2..b41d3b719b7bff 100644 --- a/.github/workflows/check-api-v2-breaking-changes.yml +++ b/.github/workflows/check-api-v2-breaking-changes.yml @@ -39,7 +39,9 @@ jobs: - name: Generate Swagger working-directory: apps/api/v2 - run: yarn generate-swagger + run: | + yarn prisma generate + yarn generate-swagger - name: Check API v2 breaking changes run: | diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index 8a4ca30454f3ec..e3e1774c805d28 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -84,6 +84,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db env: DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index 80112c3e4b4dd1..a58cac84840255 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -76,6 +76,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index 614b098d935010..c3b9b5e3c642ea 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -84,6 +84,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index a5132a89d11419..54a20cb066ce53 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -85,6 +85,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install - uses: ./.github/actions/yarn-playwright-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - uses: ./.github/actions/cache-build - name: Run Tests diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index c9c6563e88d29b..83605ea5ccba94 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -81,6 +81,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - uses: ./.github/actions/cache-db - name: Run Tests run: VITEST_MODE=integration yarn test diff --git a/.github/workflows/setup-db.yml b/.github/workflows/setup-db.yml index a4618d1fb92141..f0634a0c68502e 100644 --- a/.github/workflows/setup-db.yml +++ b/.github/workflows/setup-db.yml @@ -50,5 +50,7 @@ jobs: - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install if: inputs.DB_CACHE_HIT != 'true' + - run: yarn prisma generate + if: inputs.DB_CACHE_HIT != 'true' - uses: ./.github/actions/cache-db if: inputs.DB_CACHE_HIT != 'true' diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 34fd4c1d086bd7..a922ec9698aaad 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/dangerous-git-checkout - uses: ./.github/actions/yarn-install + - run: yarn prisma generate - run: yarn test -- --no-isolate # We could add different timezones here that we need to run our tests in - run: TZ=America/Los_Angeles VITEST_MODE=timezone yarn test -- --no-isolate From 966d4b5cc6644c65e3b02575caed556359600861 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 23:17:01 -0300 Subject: [PATCH 07/10] fix: address flaky E2E tests (#26374) * fix: address flaky E2E tests - booking-pages.e2e.ts: Use selectFirstAvailableTimeSlotNextMonth helper instead of brittle nth(1) selector to avoid race condition where time slots become unavailable - fixtures/users.ts: Add retryOnNetworkError helper to handle transient ECONNRESET errors during apiLogin - lib/testUtils.ts: Add waitForLoadState('networkidle') to goToUrlWithErrorHandling to ensure page is fully loaded before checking URL - teams.e2e.ts: Add explicit wait for publish button visibility before clicking to avoid timeout - unpublished.e2e.ts: Change from parallel to serial mode to avoid database deadlocks from concurrent writes Co-Authored-By: keith@cal.com * fix: consolidate unpublished.e2e.ts tests to reduce concurrent DB writes Instead of using serial mode, consolidate related tests into single test functions that share setup data. This reduces concurrent users.create() and users.deleteAll() calls from 7 to 3, significantly reducing the chance of database deadlocks while maintaining parallel execution. Co-Authored-By: keith@cal.com * fix: only fail goToUrlWithErrorHandling on main navigation requests The previous fix was incorrectly resolving the promise when any request failed (like images, RSC requests, etc.), causing the URL check to fail. Now we only consider it a navigation failure if: - request.isNavigationRequest() is true - request.frame() === page.mainFrame() Also added a resolved flag to prevent multiple resolutions and removed the networkidle wait which was causing issues. Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/playwright/booking-pages.e2e.ts | 13 +--- apps/web/playwright/fixtures/users.ts | 52 ++++++++++--- apps/web/playwright/lib/testUtils.ts | 19 ++++- apps/web/playwright/teams.e2e.ts | 7 +- apps/web/playwright/unpublished.e2e.ts | 97 +++++++++++------------- 5 files changed, 110 insertions(+), 78 deletions(-) diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index f38e672c5020ca..aab886487cb210 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -577,16 +577,9 @@ test.describe("Booking on different layouts", () => { await page.click('[data-testid="toggle-group-item-column_view"]'); - await page.click('[data-testid="incrementMonth"]'); - - await page.waitForURL((url) => { - return url.searchParams.has("month"); - }) - - await page.reload(); - await page.waitForLoadState("networkidle"); - - await page.locator('[data-testid="time"]').nth(1).click(); + // Use the standard helper to select an available time slot next month + // This is more robust than manually clicking incrementMonth and reloading + await selectFirstAvailableTimeSlotNextMonth(page); // Fill what is this meeting about? name email and notes await page.locator('[name="name"]').fill("Test name"); diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index e530d1fd574256..4a53966ec61ed4 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -1106,17 +1106,49 @@ export async function login( await responsePromise; } +/** + * Helper to retry network requests that may fail with transient errors like ECONNRESET + */ +async function retryOnNetworkError( + fn: () => Promise, + maxRetries = 3, + delayMs = 500 +): Promise { + let lastError: Error | undefined; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + const errorMessage = lastError.message || ""; + // Only retry on transient network errors + const isRetryable = + errorMessage.includes("ECONNRESET") || + errorMessage.includes("ECONNREFUSED") || + errorMessage.includes("ETIMEDOUT") || + errorMessage.includes("socket hang up"); + + if (!isRetryable || attempt === maxRetries) { + throw lastError; + } + // Wait before retrying with exponential backoff + await new Promise((resolve) => setTimeout(resolve, delayMs * attempt)); + } + } + throw lastError; +} + export async function apiLogin( user: Pick & Partial> & { password: string | null }, page: Page, navigateToUrl?: string ) { - // Get CSRF token - const csrfToken = await page - .context() - .request.get("/api/auth/csrf") - .then((response) => response.json()) - .then((json) => json.csrfToken); + // Get CSRF token with retry for transient network errors + const csrfToken = await retryOnNetworkError(async () => { + const response = await page.context().request.get("/api/auth/csrf"); + const json = await response.json(); + return json.csrfToken; + }); // Make the login request const loginData = { @@ -1128,9 +1160,11 @@ export async function apiLogin( csrfToken, }; - const response = await page.context().request.post("/api/auth/callback/credentials", { - data: loginData, - }); + const response = await retryOnNetworkError(() => + page.context().request.post("/api/auth/callback/credentials", { + data: loginData, + }) + ); expect(response.status()).toBe(200); diff --git a/apps/web/playwright/lib/testUtils.ts b/apps/web/playwright/lib/testUtils.ts index 98f0a61d73a673..665edf32044ab1 100644 --- a/apps/web/playwright/lib/testUtils.ts +++ b/apps/web/playwright/lib/testUtils.ts @@ -416,19 +416,32 @@ export async function fillStripeTestCheckout(page: Page) { export function goToUrlWithErrorHandling({ page, url }: { page: Page; url: string }) { return new Promise<{ success: boolean; url: string }>(async (resolve) => { + let resolved = false; const onRequestFailed = (request: PlaywrightRequest) => { + // Only consider it a navigation failure if it's the main document request + // Ignore failures for subresources like images, scripts, RSC requests, etc. + if (!request.isNavigationRequest() || request.frame() !== page.mainFrame()) { + const failedToLoadUrl = request.url(); + console.log("goToUrlWithErrorHandling: Failed to load URL:", failedToLoadUrl); + return; + } + if (resolved) return; + resolved = true; const failedToLoadUrl = request.url(); - console.log("goToUrlWithErrorHandling: Failed to load URL:", failedToLoadUrl); + console.log("goToUrlWithErrorHandling: Navigation failed for URL:", failedToLoadUrl); resolve({ success: false, url: failedToLoadUrl }); }; page.on("requestfailed", onRequestFailed); try { - await page.goto(url); + await page.goto(url, { waitUntil: "domcontentloaded" }); } catch { // do nothing } page.off("requestfailed", onRequestFailed); - resolve({ success: true, url: page.url() }); + if (!resolved) { + resolved = true; + resolve({ success: true, url: page.url() }); + } }); } diff --git a/apps/web/playwright/teams.e2e.ts b/apps/web/playwright/teams.e2e.ts index 6bc77519cd7af6..376fe4ba785736 100644 --- a/apps/web/playwright/teams.e2e.ts +++ b/apps/web/playwright/teams.e2e.ts @@ -171,8 +171,11 @@ test.describe("Teams - NonOrg", () => { // eslint-disable-next-line playwright/no-conditional-in-test if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page); await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i); - // Click text=Continue - await page.locator("[data-testid=publish-button]").click(); + // Wait for the page to fully load and the publish button to be visible + await page.waitForLoadState("networkidle"); + const publishButton = page.locator("[data-testid=publish-button]"); + await publishButton.waitFor({ state: "visible", timeout: 10000 }); + await publishButton.click(); await page.waitForURL(/\/settings\/teams\/(\d+)\/event-type*$/i); await page.locator("[data-testid=handle-later-button]").click(); await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i); diff --git a/apps/web/playwright/unpublished.e2e.ts b/apps/web/playwright/unpublished.e2e.ts index f52fc5fd491c43..ef0bf00b099057 100644 --- a/apps/web/playwright/unpublished.e2e.ts +++ b/apps/web/playwright/unpublished.e2e.ts @@ -4,16 +4,13 @@ import { SchedulingType } from "@calcom/prisma/enums"; import { test } from "./lib/fixtures"; +// Keep parallel mode - each test creates its own isolated data test.describe.configure({ mode: "parallel" }); const title = (name: string) => `${name} is unpublished`; const description = (entity: string) => `This ${entity} link is currently not available. Please contact the ${entity} owner or ask them to publish it.`; -test.afterEach(async ({ users }) => { - await users.deleteAll(); -}); - const assertChecks = async (page: any, entityName: string, entityType: string) => { await expect(page.locator('[data-testid="empty-screen"]')).toHaveCount(1); await expect(page.locator(`h2:has-text("${title(entityName)}")`)).toHaveCount(1); @@ -21,20 +18,13 @@ const assertChecks = async (page: any, entityName: string, entityType: string) = await expect(page.locator(`img`)).toHaveAttribute("src", /.*/); }; -test.describe("Unpublished", () => { - test("Regular team profile", async ({ page, users }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true }); - const { team } = await owner.getFirstTeamMembership(); - const { requestedSlug } = team.metadata as { requestedSlug: string }; - const prefixes = ["", "/en"]; - - for (const prefix of prefixes) { - await page.goto(`${prefix}/team/${requestedSlug}`); - await assertChecks(page, team.name, "team"); - } +// Group 1: Regular team tests - share setup data +test.describe("Unpublished - Regular team", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); }); - test("Regular team event type", async ({ page, users }) => { + test("Regular team profile", async ({ page, users }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, @@ -42,46 +32,63 @@ test.describe("Unpublished", () => { }); const { team } = await owner.getFirstTeamMembership(); const { requestedSlug } = team.metadata as { requestedSlug: string }; - const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id); + const teamEventSlug = (await owner.getFirstTeamEvent(team.id)).slug; const prefixes = ["", "/en"]; + // Test team profile + for (const prefix of prefixes) { + await page.goto(`${prefix}/team/${requestedSlug}`); + await assertChecks(page, team.name, "team"); + } + + // Test team event type (reuse same data) for (const prefix of prefixes) { await page.goto(`${prefix}/team/${requestedSlug}/${teamEventSlug}`); await assertChecks(page, team.name, "team"); } }); +}); + +// Group 2: Organization tests - share setup data +test.describe("Unpublished - Organization", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); + }); - test("Organization profile", async ({ users, page }) => { + test("Organization profile and user", async ({ users, page }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); const { team: org } = await owner.getOrgMembership(); const { requestedSlug } = org.metadata as { requestedSlug: string }; + const [{ slug: ownerEventType }] = owner.eventTypes; const prefixes = ["", "/en"]; + // Test organization profile for (const prefix of prefixes) { await page.goto(`${prefix}/org/${requestedSlug}`); await assertChecks(page, org.name, "organization"); } - }); - test("Organization sub-team", async ({ users, page }) => { - const owner = await users.create(undefined, { - hasTeam: true, - isUnpublished: true, - isOrg: true, - hasSubteam: true, - }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const [{ slug: subteamSlug }] = org.children as { slug: string }[]; - const prefixes = ["", "/en"]; + // Test organization user + for (const prefix of prefixes) { + await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}`); + await assertChecks(page, org.name, "organization"); + } + // Test organization user event-type for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}`); + await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}/${ownerEventType}`); await assertChecks(page, org.name, "organization"); } }); +}); + +// Group 3: Organization sub-team tests - share setup data +test.describe("Unpublished - Organization sub-team", () => { + test.afterEach(async ({ users }) => { + await users.deleteAll(); + }); - test("Organization sub-team event-type", async ({ users, page }) => { + test("Organization sub-team and event-type", async ({ users, page }) => { const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, @@ -94,33 +101,15 @@ test.describe("Unpublished", () => { const { slug: subteamEventSlug } = await owner.getFirstTeamEvent(subteamId); const prefixes = ["", "/en"]; + // Test organization sub-team for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}/${subteamEventSlug}`); - await assertChecks(page, org.name, "organization"); - } - }); - - test("Organization user", async ({ users, page }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const prefixes = ["", "/en"]; - - for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}`); + await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}`); await assertChecks(page, org.name, "organization"); } - }); - - test("Organization user event-type", async ({ users, page }) => { - const owner = await users.create(undefined, { hasTeam: true, isUnpublished: true, isOrg: true }); - const { team: org } = await owner.getOrgMembership(); - const { requestedSlug } = org.metadata as { requestedSlug: string }; - const [{ slug: ownerEventType }] = owner.eventTypes; - const prefixes = ["", "/en"]; + // Test organization sub-team event-type for (const prefix of prefixes) { - await page.goto(`${prefix}/org/${requestedSlug}/${owner.username}/${ownerEventType}`); + await page.goto(`${prefix}/org/${requestedSlug}/team/${subteamSlug}/${subteamEventSlug}`); await assertChecks(page, org.name, "organization"); } }); From 20c67ef101755d439ab723b5cb396cf7b08104e7 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 23:18:35 -0300 Subject: [PATCH 08/10] fix: handle non-deterministic AES-256-CBC decryption in crypto test (#26383) AES-256-CBC doesn't guarantee throwing on wrong key decryption - it depends on whether the decrypted bytes happen to have valid PKCS#7 padding. The test now verifies that decryption either throws OR returns a value different from the original plaintext. This fixes flaky test failures that started appearing after the Vitest 4.0 upgrade, where the 'Closing rpc while fetch was pending' error was a secondary symptom of the test failure causing worker teardown during module loading. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/lib/crypto.test.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/lib/crypto.test.ts b/packages/lib/crypto.test.ts index e323798eaf9185..498792ac360819 100644 --- a/packages/lib/crypto.test.ts +++ b/packages/lib/crypto.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; -import { symmetricEncrypt, symmetricDecrypt } from "./crypto"; +import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; describe("crypto", () => { const testKey = "12345678901234567890123456789012"; // 32 bytes key @@ -49,11 +49,24 @@ describe("crypto", () => { expect(() => symmetricDecrypt(":", testKey)).toThrow(); }); - it("should throw error if wrong key is used", () => { + it("should fail to decrypt correctly if wrong key is used", () => { const encrypted = symmetricEncrypt(testText, testKey); const wrongKey = "12345678901234567890123456789013"; // Different 32 bytes key - expect(() => symmetricDecrypt(encrypted, wrongKey)).toThrow(); + // AES-256-CBC doesn't guarantee throwing on wrong key - it depends on whether + // the decrypted bytes happen to have valid PKCS#7 padding. The test verifies + // that decryption either throws OR returns a value different from the original. + let decryptedWithWrongKey: string | null = null; + let threwError = false; + + try { + decryptedWithWrongKey = symmetricDecrypt(encrypted, wrongKey); + } catch { + threwError = true; + } + + // Either it threw an error, or the decrypted value is not the original text + expect(threwError || decryptedWithWrongKey !== testText).toBe(true); }); it("should handle empty string encryption/decryption", () => { From c7a80abcbac368f76d4a9d5dd7a0a5721d252b83 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 1 Jan 2026 23:48:04 -0300 Subject: [PATCH 09/10] chore: Make atoms cache-key more reusable (#26384) --- .github/workflows/atoms-production-build.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/atoms-production-build.yml b/.github/workflows/atoms-production-build.yml index 6094722d764c23..b8fc029accedc3 100644 --- a/.github/workflows/atoms-production-build.yml +++ b/.github/workflows/atoms-production-build.yml @@ -20,17 +20,11 @@ jobs: env: cache-name: atoms-build key-1: ${{ hashFiles('yarn.lock') }} - key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }} - key-3: ${{ github.event.pull_request.number || github.ref }} - # Ensures production-build.yml will always be fresh - key-4: ${{ github.sha }} + key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', 'packages/platform/atoms/package.json', '!**/node_modules') }} with: path: | **/dist/** - key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }} - - name: Log Cache Hit - if: steps.cache-atoms-build.outputs.cache-hit == 'true' - run: echo "Cache hit for Atoms build. Skipping build." + key: ${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }} - name: Run build if: steps.cache-atoms-build.outputs.cache-hit != 'true' run: | From 18b041514f4e1efea756968faf96a45da3f02918 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 2 Jan 2026 09:41:14 +0530 Subject: [PATCH 10/10] refactor(ui): remove unused components and exports (#26222) Co-authored-by: Volnei Munhoz Co-authored-by: Keith Williams --- .../BookEventForm/BookingFields.test.tsx | 215 ------------------ packages/ui/components/address/index.ts | 1 - .../ui/components/badge/UpgradeOrgsBadge.tsx | 16 -- packages/ui/components/badge/index.ts | 1 - packages/ui/components/dropdown/index.ts | 4 - packages/ui/components/form/index.ts | 11 +- packages/ui/components/form/inputs/Input.tsx | 27 --- .../form/inputs/InputFieldWithSelect.tsx | 20 -- .../ui/components/form/inputs/input.test.tsx | 28 --- .../ui/components/form/slider/RangeSlider.tsx | 25 -- .../form/slider/RangeSliderPopover.tsx | 129 ----------- packages/ui/components/form/slider/index.tsx | 25 -- packages/ui/components/form/step/FormStep.tsx | 34 --- packages/ui/components/form/step/Stepper.tsx | 71 ------ packages/ui/components/form/step/index.ts | 2 - .../components/navigation/NavigationItem.tsx | 101 -------- packages/ui/components/navigation/index.ts | 1 - 17 files changed, 1 insertion(+), 710 deletions(-) delete mode 100644 packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx delete mode 100644 packages/ui/components/badge/UpgradeOrgsBadge.tsx delete mode 100644 packages/ui/components/form/inputs/InputFieldWithSelect.tsx delete mode 100644 packages/ui/components/form/slider/RangeSlider.tsx delete mode 100644 packages/ui/components/form/slider/RangeSliderPopover.tsx delete mode 100644 packages/ui/components/form/slider/index.tsx delete mode 100644 packages/ui/components/form/step/FormStep.tsx delete mode 100644 packages/ui/components/form/step/Stepper.tsx delete mode 100644 packages/ui/components/navigation/NavigationItem.tsx diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx deleted file mode 100644 index f21af3756ffd0a..00000000000000 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.test.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { TooltipProvider } from "@radix-ui/react-tooltip"; -import { render, fireEvent, screen } from "@testing-library/react"; -import * as React from "react"; -import type { UseFormReturn } from "react-hook-form"; -import { FormProvider, useForm } from "react-hook-form"; -import { expect, vi } from "vitest"; - -import PhoneInput from "@calcom/features/components/phone-input/PhoneInput"; - -import { getBookingFieldsWithSystemFields } from "../../../lib/getBookingFields"; -import { BookingFields } from "./BookingFields"; - -// Mock PhoneInput to avoid calling the lazy import -vi.mock("@calcom/features/components/phone-input", () => { - return { - default: PhoneInput, - }; -}); - -vi.mock("@calcom/ui/components/address", async (originalImport) => { - const { AddressInputNonLazy } = (await originalImport()) as Record; - // Dynamic imports of Components are not supported in Vitest. So, we use the non-lazy version of the components - return { - AddressInput: AddressInputNonLazy, - }; -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type FormMethods = UseFormReturn; - -// Add tRPC mock before tests -vi.mock("@calcom/trpc/react", () => ({ - trpc: { - viewer: { - public: { - countryCode: { - useQuery: () => ({ - data: { countryCode: "US" }, - isLoading: false, - error: null, - }), - }, - }, - }, - }, -})); - -const renderComponent = ({ - props: props, - formDefaultValues, -}: { - props: Parameters[0]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - formDefaultValues?: any; -}) => { - let formMethods: UseFormReturn | undefined; - const Wrapper = ({ children }: { children: React.ReactNode }) => { - const form = useForm({ - defaultValues: formDefaultValues, - }); - formMethods = form; - return ( - - {children} - - ); - }; - const result = render(, { wrapper: Wrapper }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return { result, formMethods: formMethods! }; -}; - -describe("BookingFields", () => { - it("should correctly render with location fields", () => { - const AttendeePhoneNumberOption = { - label: "attendee_phone_number", - value: "phone", - }; - - const OrganizerLinkOption = { - label: "https://google.com", - value: "link", - }; - - const locations = [ - { - type: AttendeePhoneNumberOption.value, - }, - { - link: "https://google.com", - type: OrganizerLinkOption.value, - displayLocationPublicly: true, - }, - ]; - const { formMethods } = renderComponent({ - props: { - fields: getBookingFieldsWithSystemFields({ - disableGuests: false, - bookingFields: [], - metadata: null, - workflows: [], - customInputs: [], - }), - locations, - isDynamicGroupBooking: false, - bookingData: null, - }, - formDefaultValues: {}, - }); - - component.fillName({ value: "John Doe" }); - component.fillEmail({ value: "john.doe@example.com" }); - component.fillNotes({ value: "This is a note" }); - expectScenarios.expectNameToBe({ value: "John Doe", formMethods }); - expectScenarios.expectEmailToBe({ value: "john.doe@example.com", formMethods }); - expectScenarios.expectNotesToBe({ value: "This is a note", formMethods }); - - component.fillRadioInputLocation({ label: AttendeePhoneNumberOption.label, inputValue: "+1234567890" }); - expectScenarios.expectLocationToBe({ - formMethods, - label: AttendeePhoneNumberOption.label, - toMatch: { - formattedValue: "+1 (234) 567-890", - value: { optionValue: "+1234567890", value: AttendeePhoneNumberOption.value }, - }, - }); - - component.fillRadioInputLocation({ label: OrganizerLinkOption.label }); - expectScenarios.expectLocationToBe({ - formMethods, - label: OrganizerLinkOption.label, - toMatch: { - formattedValue: "+1 (234) 567-890", - value: { optionValue: "", value: OrganizerLinkOption.value }, - }, - }); - }); -}); - -const component = { - getName: ({ label = "your_name" }: { label?: string } = {}) => - screen.getByRole("textbox", { - name: new RegExp(label), - }) as HTMLInputElement, - getEmail: () => screen.getByRole("textbox", { name: /email/i }) as HTMLInputElement, - getLocationRadioOption: ({ label }: { label: string }) => - screen.getByRole("radio", { name: new RegExp(label) }) as HTMLInputElement, - getLocationRadioInput: ({ placeholder }: { placeholder: string }) => - screen.getByPlaceholderText(placeholder) as HTMLInputElement, - getNotes: () => screen.getByRole("textbox", { name: /additional_notes/i }) as HTMLInputElement, - getGuests: () => screen.getByLabelText("guests"), - fillName: ({ value }: { value: string }) => { - fireEvent.change(component.getName(), { target: { value } }); - }, - fillEmail: ({ value }: { value: string }) => { - fireEvent.change(component.getEmail(), { target: { value } }); - }, - fillRadioInputLocation: ({ label, inputValue }: { label: string; inputValue?: string }) => { - fireEvent.click(component.getLocationRadioOption({ label })); - - if (inputValue) { - let placeholder = label; - if (label === "attendee_phone_number") { - placeholder = "enter_phone_number"; - } else { - // radioInput doesn't have a label, so we need to identify by placeholder - throw new Error("Tell me how to identify the placeholder for this location input"); - } - fireEvent.change(component.getLocationRadioInput({ placeholder }), { - target: { value: inputValue }, - }); - } - }, - fillNotes: ({ value }: { value: string }) => { - fireEvent.change(component.getNotes(), { target: { value } }); - }, -}; - -const expectScenarios = { - expectNameToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getName().value).toEqual(value); - expect(formMethods.getValues("responses.name")).toEqual(value); - }, - expectEmailToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getEmail().value).toEqual(value); - expect(formMethods.getValues("responses.email")).toEqual(value); - }, - expectLocationToBe: ({ - formMethods, - label, - toMatch: { formattedValue, value }, - }: { - label: string; - toMatch: { - formattedValue?: string; - value: { - optionValue: string; - value: string; - }; - }; - formMethods: FormMethods; - }) => { - expect(component.getLocationRadioOption({ label }).checked).toBe(true); - if (value.optionValue) { - expect(component.getLocationRadioInput({ placeholder: "enter_phone_number" }).value).toEqual( - formattedValue - ); - } - expect(formMethods.getValues("responses.location")).toEqual(value); - }, - expectNotesToBe: ({ value, formMethods }: { value: string; formMethods: FormMethods }) => { - expect(component.getNotes().value).toEqual(value); - expect(formMethods.getValues("responses.notes")).toEqual(value); - }, -}; diff --git a/packages/ui/components/address/index.ts b/packages/ui/components/address/index.ts index 09ecbf71f88bc6..03d621d09f2a2b 100644 --- a/packages/ui/components/address/index.ts +++ b/packages/ui/components/address/index.ts @@ -1,3 +1,2 @@ export { default as AddressInput } from "./AddressInputLazy"; export { default as MultiEmail } from "./MultiEmailLazy"; -export { default as AddressInputNonLazy } from "./AddressInput"; diff --git a/packages/ui/components/badge/UpgradeOrgsBadge.tsx b/packages/ui/components/badge/UpgradeOrgsBadge.tsx deleted file mode 100644 index 932cd82497cad8..00000000000000 --- a/packages/ui/components/badge/UpgradeOrgsBadge.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useLocale } from "@calcom/lib/hooks/useLocale"; - -import { Tooltip } from "../tooltip"; -import { Badge } from "./Badge"; - -export const UpgradeOrgsBadge = function UpgradeOrgsBadge() { - const { t } = useLocale(); - - return ( - - - {t("upgrade")} - - - ); -}; diff --git a/packages/ui/components/badge/index.ts b/packages/ui/components/badge/index.ts index b1ee3f44ef3e03..e46de5a423ed6a 100644 --- a/packages/ui/components/badge/index.ts +++ b/packages/ui/components/badge/index.ts @@ -1,6 +1,5 @@ export { Badge } from "./Badge"; export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge"; export { CreditsBadge } from "./CreditsBadge"; -export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge"; export { InfoBadge } from "./InfoBadge"; export type { BadgeProps } from "./Badge"; diff --git a/packages/ui/components/dropdown/index.ts b/packages/ui/components/dropdown/index.ts index 934ddb81c83468..f7dd2fec044c05 100644 --- a/packages/ui/components/dropdown/index.ts +++ b/packages/ui/components/dropdown/index.ts @@ -4,13 +4,9 @@ export { DropdownItem, DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, - DropdownMenuTriggerItem, } from "./Dropdown"; diff --git a/packages/ui/components/form/index.ts b/packages/ui/components/form/index.ts index bdd055ee30a4be..50ecec3624559e 100644 --- a/packages/ui/components/form/index.ts +++ b/packages/ui/components/form/index.ts @@ -1,12 +1,8 @@ export { Checkbox, MultiSelectCheckbox, CheckboxField } from "./checkbox"; export type { Option as MultiSelectCheckboxesOptionType } from "./checkbox"; -export { HintsOrErrors } from "./inputs/HintOrErrors"; export { EmailField, EmailInput, - FieldsetLegend, - InputGroupBox, - InputLeading, PasswordField, TextArea, TextAreaField, @@ -15,8 +11,6 @@ export { } from "./inputs/Input"; export { MultiOptionInput } from "./inputs/MultiOptionInput"; - -export { InputFieldWithSelect } from "./inputs/InputFieldWithSelect"; export type { InputFieldProps, InputProps } from "./inputs/types"; export { InputField, Input, TextField, inputStyles } from "./inputs/TextField"; export { InputError } from "./inputs/InputError"; @@ -25,12 +19,9 @@ export { Label } from "./inputs/Label"; export { Select, SelectField, SelectWithValidation, getReactSelectProps } from "./select"; export { DateRangePickerLazy as DateRangePicker } from "./date-range-picker"; -export { Slider } from "./slider"; -export { RangeSlider } from "./slider/RangeSlider"; -export { RangeSliderPopover } from "./slider/RangeSliderPopover"; export { BooleanToggleGroup, BooleanToggleGroupField, ToggleGroup } from "./toggleGroup"; export { DatePicker } from "./datepicker"; -export { FormStep, Steps, Stepper } from "./step"; +export { Steps } from "./step"; export { WizardForm } from "./wizard"; export { default as ColorPicker } from "./color-picker/colorpicker"; export { SettingsToggle, Switch } from "./switch"; diff --git a/packages/ui/components/form/inputs/Input.tsx b/packages/ui/components/form/inputs/Input.tsx index 6f217e25dd1765..88a5bbd734a723 100644 --- a/packages/ui/components/form/inputs/Input.tsx +++ b/packages/ui/components/form/inputs/Input.tsx @@ -14,15 +14,6 @@ import { Input, InputField, inputStyles } from "../inputs/TextField"; import { Label } from "./Label"; import type { InputFieldProps } from "./types"; -export function InputLeading(props: JSX.IntrinsicElements["div"]) { - return ( - - {props.children} - - ); -} - - export const PasswordField = forwardRef(function PasswordField( props, ref @@ -149,24 +140,6 @@ export const TextAreaField = forwardRef ); }); -export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) { - return ( - - {props.children} - - ); -} - -export function InputGroupBox(props: JSX.IntrinsicElements["div"]) { - return ( -
- {props.children} -
- ); -} - export const NumberInput = forwardRef(function NumberInput(props, ref) { return ( (function EmailField(props, ref) { - return ( - } - /> - ); -}); diff --git a/packages/ui/components/form/inputs/input.test.tsx b/packages/ui/components/form/inputs/input.test.tsx index fc0d6e63580b68..4de924b967f3d9 100644 --- a/packages/ui/components/form/inputs/input.test.tsx +++ b/packages/ui/components/form/inputs/input.test.tsx @@ -1,11 +1,8 @@ -/* eslint-disable playwright/missing-playwright-await */ import { TooltipProvider } from "@radix-ui/react-tooltip"; import { render, fireEvent } from "@testing-library/react"; import { vi } from "vitest"; -import type { UnstyledSelect } from "../../address/Select"; import { EmailField, TextAreaField, PasswordField, NumberInput, FilterSearchField } from "./Input"; -import { InputFieldWithSelect } from "./InputFieldWithSelect"; import { InputField } from "./TextField"; const onChangeMock = vi.fn(); @@ -117,31 +114,6 @@ describe("Tests for TextAreaField Component", () => { }); }); -describe("Tests for InputFieldWithSelect Component", () => { - test("Should render correctly with InputField and UnstyledSelect", () => { - const onChangeMock = vi.fn(); - - const selectProps = { - value: null, - onChange: onChangeMock, - name: "testSelect", - options: [ - { value: "Option 1", label: "Option 1" }, - { value: "Option 2", label: "Option 2" }, - { value: "Option 3", label: "Option 3" }, - ], - } as unknown as typeof UnstyledSelect; - - const { getByText } = render(); - - const inputElement = getByText("Select..."); - fireEvent.mouseDown(inputElement); - - const optionElement = getByText("Option 1"); - expect(optionElement).toBeInTheDocument(); - }); -}); - describe("Tests for NumberInput Component", () => { test("Should render correctly with input type number", () => { const { getByRole } = render(); diff --git a/packages/ui/components/form/slider/RangeSlider.tsx b/packages/ui/components/form/slider/RangeSlider.tsx deleted file mode 100644 index cbbfbb2f8280d1..00000000000000 --- a/packages/ui/components/form/slider/RangeSlider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import * as SliderPrimitive from "@radix-ui/react-slider"; -import * as React from "react"; - -import classNames from "@calcom/ui/classNames"; - -const RangeSlider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - - -)); -RangeSlider.displayName = SliderPrimitive.Root.displayName; - -export { RangeSlider }; diff --git a/packages/ui/components/form/slider/RangeSliderPopover.tsx b/packages/ui/components/form/slider/RangeSliderPopover.tsx deleted file mode 100644 index 4f84dbd9e1cb14..00000000000000 --- a/packages/ui/components/form/slider/RangeSliderPopover.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import * as Popover from "@radix-ui/react-popover"; -import { useState } from "react"; - -import { Badge } from "../../badge/Badge"; -import { Button } from "../../button/Button"; -import { inputStyles, TextField } from "../inputs/TextField"; -import { RangeSlider } from "./RangeSlider"; - -interface RangeSliderPopoverProps { - triggerText: string; - resetBtnText?: string; - applyBtnText?: string; - value: number[]; - onChange: (value: number[]) => void; - min: number; - max: number; - step?: number; - badgeVariant?: "default" | "success" | "gray" | "warning" | "orange" | "red"; - badgeSuffix?: string; - inputSuffix?: string; - inputLeading?: string; -} - -export const RangeSliderPopover = ({ - resetBtnText = "Reset", - applyBtnText = "Apply", - triggerText, - value, - onChange, - min, - max, - step = 1, - badgeVariant = "default", - badgeSuffix, - inputSuffix, - inputLeading, -}: RangeSliderPopoverProps) => { - const [internalValue, setInternalValue] = useState(value); - const [open, setOpen] = useState(false); - - const handleReset = () => { - setInternalValue([min, max]); - }; - - const handleApply = () => { - onChange(internalValue); - setOpen(false); - }; - - return ( - - - - - - -
- -
-
- { - const newValue = parseInt(e.target.value); - if (!isNaN(newValue) && newValue >= min && newValue <= internalValue[1]) { - setInternalValue([newValue, internalValue[1]]); - } - }} - addOnLeading={inputLeading ? inputLeading : undefined} - addOnSuffix={inputSuffix ? inputSuffix : undefined} - containerClassName="w-full" - /> - { - const newValue = parseInt(e.target.value); - if (!isNaN(newValue) && newValue >= internalValue[0] && newValue <= max) { - setInternalValue([internalValue[0], newValue]); - } - }} - addOnLeading={inputLeading ? inputLeading : undefined} - addOnSuffix={inputSuffix ? inputSuffix : undefined} - containerClassName="w-full" - /> -
- -
-
-
- - -
-
-
-
-
- ); -}; diff --git a/packages/ui/components/form/slider/index.tsx b/packages/ui/components/form/slider/index.tsx deleted file mode 100644 index 254ca364afa1cd..00000000000000 --- a/packages/ui/components/form/slider/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import * as SliderPrimitive from "@radix-ui/react-slider"; -import * as React from "react"; - -import classNames from "@calcom/ui/classNames"; - -const Slider = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - - - -)); -Slider.displayName = SliderPrimitive.Root.displayName; - -export { Slider }; diff --git a/packages/ui/components/form/step/FormStep.tsx b/packages/ui/components/form/step/FormStep.tsx deleted file mode 100644 index b0fec7ba08815c..00000000000000 --- a/packages/ui/components/form/step/FormStep.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; - -import classNames from "@calcom/ui/classNames"; - -type Props = { - steps: number; - currentStep: number; -}; - -// It might be worth passing this label string from outside the component so we can translate it? -function FormStep({ currentStep, steps }: Props) { - return ( -
-

- Step {currentStep} of {steps} -

-
- {[...Array(steps)].map((_, j) => { - return ( -
= j ? "bg-black" : "bg-gray-400" - )} - key={j} - /> - ); - })} -
-
- ); -} - -export default FormStep; diff --git a/packages/ui/components/form/step/Stepper.tsx b/packages/ui/components/form/step/Stepper.tsx deleted file mode 100644 index fa75f4011268cd..00000000000000 --- a/packages/ui/components/form/step/Stepper.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import Link from "next/link"; - -type DefaultStep = { - title: string; -}; - -function Stepper(props: { - href: string; - step: number; - steps: T[]; - disableSteps?: boolean; - stepLabel?: (currentStep: number, totalSteps: number) => string; -}) { - const { - href, - steps, - stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`, - } = props; - const [stepperRef] = useAutoAnimate(); - return ( - <> - {steps.length > 1 && ( - - )} - - ); -} - -export default Stepper; diff --git a/packages/ui/components/form/step/index.ts b/packages/ui/components/form/step/index.ts index 14316fa07eacfb..bcd0c54fa1d966 100644 --- a/packages/ui/components/form/step/index.ts +++ b/packages/ui/components/form/step/index.ts @@ -1,3 +1 @@ -export { default as FormStep } from "./FormStep"; export { Steps } from "./Steps"; -export { default as Stepper } from "./Stepper"; diff --git a/packages/ui/components/navigation/NavigationItem.tsx b/packages/ui/components/navigation/NavigationItem.tsx deleted file mode 100644 index c6c96832b5e8a2..00000000000000 --- a/packages/ui/components/navigation/NavigationItem.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { cva } from "class-variance-authority"; -import { Fragment } from "react"; - -import classNames from "@calcom/ui/classNames"; - -import { ButtonOrLink } from "../dropdown"; -import { Icon } from "../icon"; -import type { IconName } from "../icon"; - -export type NavigationItemType = { - isLastChild?: boolean; - isExpanded?: boolean; - onToggle?: () => void; - name: string; - href?: string; - isLoading?: boolean; - badge?: React.ReactNode; - icon?: IconName; - child?: NavigationItemType[]; - onlyMobile?: boolean; - onlyDesktop?: boolean; - moreOnMobile?: boolean; - isCurrent?: boolean; -}; - -const navigationItemStyles = cva( - "text-default group flex items-center rounded-[10px] p-2 text-sm font-medium transition hover:bg-subtle hover:text-emphasis", - { - variants: { - isChild: { - true: "[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 ml-16 lg:flex lg:ml-10 relative before:absolute before:left-[-24px] before:-top-2 before:h-[calc(100%+0.5rem)] before:w-0.5 before:bg-subtle before:content-[''] first:before:rounded-t-full last:before:rounded-b-full", - false: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm", - }, - hasChild: { - true: "aria-[aria-current='page']:bg-transparent! relative after:absolute after:left-[-24px] after:top-6 after:h-[calc(100%-1.5rem)] after:w-0.5 after:bg-subtle after:content-[''] first:after:rounded-t-full last:after:rounded-b-full", - false: "[&[aria-current='page']]:bg-subtle", - }, - isFirstChild: { - true: "mt-0", - false: "mt-px", - }, - }, - defaultVariants: { - isChild: false, - hasChild: false, - isFirstChild: false, - }, - } -); - -const Label = ({ children }: { children: React.ReactNode }) => { - return {children}; -}; - -const NavigationItemComponent = ({ - item, - isChild, - index, -}: { - item: NavigationItemType; - isChild?: boolean; - index?: number; -}) => { - return ( - - - {item.icon && ( - - {item.child && - item.isExpanded && - item.child.map((childItem, childIndex) => ( - - ))} - - ); -}; - -export const NavigationItem = Object.assign(NavigationItemComponent, { Label }); diff --git a/packages/ui/components/navigation/index.ts b/packages/ui/components/navigation/index.ts index aef29ad145bf34..a304de05d83cb0 100644 --- a/packages/ui/components/navigation/index.ts +++ b/packages/ui/components/navigation/index.ts @@ -5,4 +5,3 @@ export type { NavTabProps } from "./tabs/HorizontalTabs"; export { default as VerticalTabItem } from "./tabs/VerticalTabItem"; export type { VerticalTabItemProps } from "./tabs/VerticalTabItem"; export { default as VerticalTabs } from "./tabs/VerticalTabs"; -export { NavigationItem } from "./NavigationItem";