Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .easignore
Original file line number Diff line number Diff line change
@@ -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
4 changes: 1 addition & 3 deletions .github/actions/yarn-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/api-v2-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
10 changes: 2 additions & 8 deletions .github/workflows/atoms-production-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/check-api-v2-breaking-changes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
8 changes: 1 addition & 7 deletions .github/workflows/docs-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-app-store.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-embed-react.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e-embed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/setup-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
1 change: 1 addition & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -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.*
13 changes: 11 additions & 2 deletions apps/web/components/apps/routing-forms/TestFormDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
13 changes: 3 additions & 10 deletions apps/web/playwright/booking-pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
52 changes: 43 additions & 9 deletions apps/web/playwright/fixtures/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
fn: () => Promise<T>,
maxRetries = 3,
delayMs = 500
): Promise<T> {
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<User, "username"> & Partial<Pick<User, "email">> & { 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 = {
Expand All @@ -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);

Expand Down
19 changes: 16 additions & 3 deletions apps/web/playwright/lib/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() });
}
});
}

Expand Down
Loading
Loading