Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3584227
chore: cleanup test data
supalarry Sep 17, 2025
d3b544d
feat: e2e env file
supalarry Sep 17, 2025
fb0f967
feat: e2e examples app prisma client
supalarry Sep 17, 2025
0520746
fix: managed user setup from example app on any port
supalarry Sep 17, 2025
3f5fffc
chore: reduce flaky e2e tests by refreshing page
supalarry Sep 17, 2025
d7e484a
feat: e2e enable new env.e2e and data cleanup
supalarry Sep 17, 2025
4629e6e
feat: e2e oauth client in a seed
supalarry Sep 17, 2025
6cca42a
feat: run atoms e2e in CI with local v2
supalarry Sep 17, 2025
d852b6a
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
supalarry Sep 23, 2025
58e470d
refactor: load e2e env only locally
supalarry Sep 23, 2025
32bc7c6
chore: reset test db in CI and ignore test db
supalarry Sep 23, 2025
d24c4da
refactor: remove unneeded cleanup
supalarry Sep 23, 2025
d23dfaa
refactor: db cleanup in finally statement
supalarry Sep 23, 2025
1c06815
refactor: use webserv.env instead of inline env
supalarry Sep 23, 2025
37cded3
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
supalarry Sep 24, 2025
c3cf330
chore: temporarily dont run atoms on e2e label
supalarry Sep 24, 2025
57eb4b0
fix: self trigger path
supalarry Sep 24, 2025
5321633
fix: increase timeout
supalarry Sep 24, 2025
3302ae8
chore: add vapid env keys to workflow
supalarry Sep 24, 2025
6ea0bda
chore: add CI_JWT_SECRET to e2e atoms
ThyMinimalDev Sep 24, 2025
cf1177e
chore: add NODE_ENV env
supalarry Sep 25, 2025
b3f33b2
fix: only run atoms e2e on main repo
supalarry Sep 25, 2025
d411d1b
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
supalarry Sep 25, 2025
0455202
fix: merge conflict
supalarry Sep 25, 2025
54ff08f
debug: add logs to check env variables
supalarry Sep 25, 2025
edb6ca8
Revert "debug: add logs to check env variables"
supalarry Sep 25, 2025
5649b61
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
ThyMinimalDev Sep 25, 2025
6392c50
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
ThyMinimalDev Sep 25, 2025
171cdbd
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
supalarry Sep 26, 2025
fb3f99d
Merge branch 'main' into lauris/cal-6422-refactor-atoms-e2e-setup
supalarry Oct 3, 2025
28b8487
try to make it work
supalarry Oct 3, 2025
4aa6f96
Revert "try to make it work"
supalarry Oct 3, 2025
346be39
upload atoms dist for inspection
supalarry Oct 3, 2025
ccfd52e
experiment: set random url instead of empty
supalarry Oct 3, 2025
e202aa7
Revert "experiment: set random url instead of empty"
supalarry Oct 3, 2025
aad1c0c
try fix
supalarry Oct 3, 2025
2e9d0b3
Revert "try fix"
supalarry Oct 3, 2025
642ccb8
try fix
supalarry Oct 3, 2025
cf2dc40
Revert "try fix"
supalarry Oct 3, 2025
a82b845
try fix
supalarry Oct 3, 2025
f14758e
Revert "try fix"
supalarry Oct 3, 2025
494ea2a
try chatgpt fix
supalarry Oct 3, 2025
c2cb435
try chatgpt fix pt 2
supalarry Oct 3, 2025
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
98 changes: 95 additions & 3 deletions .github/workflows/e2e-atoms.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,93 @@
name: E2E Atoms

on:
workflow_call:
pull_request:
branches:
- main
paths:
- 'packages/platform/atoms/**'
- 'packages/platform/examples/base/**'
- 'apps/api/v2/**'
- '.github/workflows/e2e-atoms.yml'

permissions:
actions: write
contents: read

env:
NODE_OPTIONS: --max-old-space-size=8096
## api v2 env
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
API_KEY_PREFIX: ${{ secrets.CI_API_KEY_PREFIX }}
API_PORT: ${{ vars.CI_API_V2_PORT }}
CALCOM_LICENSE_KEY: ${{ secrets.CI_CALCOM_LICENSE_KEY }}
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
DATABASE_READ_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_WRITE_URL: ${{ secrets.CI_DATABASE_URL }}
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
IS_E2E: true
NEXTAUTH_SECRET: ${{ secrets.CI_NEXTAUTH_SECRET }}
NEXTAUTH_URL: ${{ secrets.CI_NEXTAUTH_URL }}
REDIS_URL: "redis://localhost:6379"
LINGO_DOT_DEV_API_KEY: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_API_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
SLOTS_CACHE_TTL: ${{ secrets.CI_SLOTS_CACHE_TTL }}
NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.NEXT_PUBLIC_VAPID_PUBLIC_KEY }}
VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }}
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
NODE_ENV: ${{ vars.CI_NODE_ENV }}

## atoms e2e examples app env
ATOMS_E2E_OAUTH_CLIENT_ID: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID }}
ATOMS_E2E_OAUTH_CLIENT_SECRET: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_SECRET }}
ATOMS_E2E_API_URL: ${{ secrets.ATOMS_E2E_API_URL }}
ATOMS_E2E_ORG_ID: ${{ secrets.ATOMS_E2E_ORG_ID }}
ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED: ${{ secrets.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED }}
ATOMS_E2E_APPLE_ID: ${{ secrets.ATOMS_E2E_APPLE_ID }}
ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE: ${{ secrets.ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE }}

## env variables needed for both api v2 and examples app
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
NODE_OPTIONS: --max-old-space-size=29000

jobs:
e2e-atoms:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
timeout-minutes: 15
name: E2E Atoms
runs-on: buildjet-4vcpu-ubuntu-2204
runs-on: buildjet-16vcpu-ubuntu-2204
services:
postgres:
image: postgres:13
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: calendso
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:latest
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: docker/login-action@v3
with:
Expand All @@ -31,10 +96,37 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/dangerous-git-checkout
- uses: ./.github/actions/yarn-install
- uses: ./.github/actions/cache-db
- uses: ./.github/actions/yarn-playwright-install
- name: Start API v2
working-directory: apps/api/v2
run: |
yarn dev:no-docker &
API_PID=$!
echo "API_PID=$API_PID" >> $GITHUB_ENV

# Wait for API to be ready
echo "Waiting for API v2 to be ready on port ${{ vars.CI_API_V2_PORT }}..."
timeout 300 bash -c 'until curl -f http://localhost:${{ vars.CI_API_V2_PORT }}/health > /dev/null 2>&1; do sleep 2; done'
echo "API v2 is ready!"
- name: Run E2E Atoms Tests
working-directory: packages/platform/examples/base
run: yarn test:e2e
- name: Upload Atoms dist (entire folder)
uses: actions/upload-artifact@v4
if: always()
with:
name: atoms-dist-${{ github.sha }}
path: packages/platform/atoms/dist
retention-days: 7
if-no-files-found: warn

- name: Stop API v2
if: always()
run: |
if [ ! -z "$API_PID" ]; then
kill $API_PID || true
fi
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,11 @@ describe("OAuth Client Users Endpoints", () => {
} catch (e) {
// User might have been deleted by the test
}
try {
await userRepositoryFixture.delete(postResponseDataTwo.user.id);
} catch (e) {
// User might have been deleted by the test
}
try {
await userRepositoryFixture.delete(platformAdmin.id);
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions packages/features/shell/useBottomNavItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function useBottomNavItems({
isTrial
? {
name: "skip_trial",
href: "",
href: publicPageUrl,
isLoading: skipTeamTrialsMutation.isPending,
icon: "clock",
onClick: (e: { preventDefault: () => void }) => {
Expand All @@ -55,7 +55,7 @@ export function useBottomNavItems({
},
{
name: "copy_public_page_link",
href: "",
href: publicPageUrl,
onClick: (e: { preventDefault: () => void }) => {
e.preventDefault();
navigator.clipboard.writeText(publicPageUrl);
Expand Down
9 changes: 9 additions & 0 deletions packages/platform/examples/base/.env.e2e.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
NEXT_PUBLIC_X_CAL_ID=
X_CAL_SECRET_KEY=
NEXT_PUBLIC_CALCOM_API_URL=
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID=
VITE_BOOKER_EMBED_API_URL=
ORGANIZATION_ID=
ATOMS_E2E_APPLE_ID=
ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE=
NEXT_PUBLIC_IS_E2E="1"
2 changes: 2 additions & 0 deletions packages/platform/examples/base/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ yarn-error.log*

# local env files
.env*.local
!.env.e2e.example

# vercel
.vercel
Expand All @@ -36,6 +37,7 @@ yarn-error.log*
next-env.d.ts
.yarn
dev.db
test.db

# playwright
test-results
Expand Down
113 changes: 113 additions & 0 deletions packages/platform/examples/base/global-teardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import dotenv from "dotenv";
import fs from "fs";
import path from "path";

dotenv.config({ path: path.resolve(__dirname, ".env.e2e") });
async function globalTeardown() {
console.log("Cleaning up managed users...");
try {
const oauthClientId = process.env.NEXT_PUBLIC_X_CAL_ID;
const secretKey = process.env.X_CAL_SECRET_KEY;
const apiUrl = process.env.NEXT_PUBLIC_CALCOM_API_URL;
if (!oauthClientId || !secretKey || !apiUrl) {
console.log("Missing environment variables, skipping managed user cleanup");
return;
}
const getManagedUsersResponse = await fetch(`${apiUrl}/oauth-clients/${oauthClientId}/users`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": secretKey,
"x-cal-client-id": oauthClientId,
},
});
if (getManagedUsersResponse.ok) {
const managedUsersData = await getManagedUsersResponse.json();
const users = managedUsersData.data || [];
for (const user of users) {
try {
const deleteResponse = await fetch(`${apiUrl}/oauth-clients/${oauthClientId}/users/${user.id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": secretKey,
"x-cal-client-id": oauthClientId,
},
});
if (deleteResponse.ok) {
console.log(`Deleted managed user: ${user.email}`);
} else {
console.error(`Failed to delete user ${user.email}:`, await deleteResponse.text());
}
} catch (error) {
console.error(`Error deleting user ${user.email}:`, error);
}
}
} else {
console.error("Failed to fetch managed users:", await getManagedUsersResponse.text());
}
const getOAuthClientResponse = await fetch(`${apiUrl}/oauth-clients/${oauthClientId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": secretKey,
"x-cal-client-id": oauthClientId,
},
});
if (getOAuthClientResponse.ok) {
const oauthClientData = await getOAuthClientResponse.json();
const organizationId = oauthClientData.data?.organizationId;
if (organizationId) {
console.log(`Found organizationId: ${organizationId}`);
const getTeamsResponse = await fetch(`${apiUrl}/organizations/${organizationId}/teams`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": secretKey,
"x-cal-client-id": oauthClientId,
},
});
if (getTeamsResponse.ok) {
const teamsData = await getTeamsResponse.json();
const teams = teamsData.data || [];
for (const team of teams) {
try {
const deleteTeamResponse = await fetch(
`${apiUrl}/organizations/${organizationId}/teams/${team.id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
"x-cal-secret-key": secretKey,
"x-cal-client-id": oauthClientId,
},
}
);
if (deleteTeamResponse.ok) {
console.log(`Deleted team: ${team.name}`);
} else {
console.error(`Failed to delete team ${team.name}:`, await deleteTeamResponse.text());
}
} catch (error) {
console.error(`Error deleting team ${team.name}:`, error);
}
}
} else {
console.error("Failed to fetch teams:", await getTeamsResponse.text());
}
} else {
console.log("No organizationId found in OAuth client");
}
} else {
console.error("Failed to fetch OAuth client:", await getOAuthClientResponse.text());
}
} catch (error) {
console.error("Failed to clean up:", error);
} finally {
console.log("Cleaning up test database...");
const testDbPath = path.resolve(__dirname, "prisma", "test.db");
fs.rmSync(testDbPath, { force: true });
console.log("Test database cleaned up successfully");
}
}
export default globalTeardown;
5 changes: 4 additions & 1 deletion packages/platform/examples/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
"start": "next start",
"lint": "eslint .",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui"
"test:e2e:ui": "playwright test --ui",
"db:push:test": "prisma db push --schema=prisma/schema.test.prisma",
"db:generate:test": "prisma generate --schema=prisma/schema.test.prisma",
"db:reset:test": "rm -f prisma/test.db && yarn db:push:test"
},
"dependencies": {
"@calcom/atoms": "workspace:*",
Expand Down
25 changes: 20 additions & 5 deletions packages/platform/examples/base/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";

const envPath = process.env.CI ? path.resolve(__dirname, ".env") : path.resolve(__dirname, ".env.local");

dotenv.config({ path: envPath });
if (!process.env.CI) {
dotenv.config({ path: path.resolve(__dirname, ".env.e2e") });
}

const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 30000 : 120000;
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 240000;
Expand All @@ -17,6 +17,7 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined,
timeout: DEFAULT_TEST_TIMEOUT,
fullyParallel: false,
globalTeardown: require.resolve("./global-teardown"),
reporter: [
["list"],
["html", { outputFolder: "./test-results/reports/playwright-html-report", open: "never" }],
Expand Down Expand Up @@ -44,10 +45,24 @@ export default defineConfig({
],
webServer: {
command: process.env.CI
? `yarn workspace @calcom/atoms dev-on && yarn workspace @calcom/atoms build && rm -f prisma/dev.db && yarn prisma db push && NEXT_PUBLIC_IS_E2E=1 NODE_ENV=test NEXT_PUBLIC_X_CAL_ID="${process.env.ATOMS_E2E_OAUTH_CLIENT_ID}" X_CAL_SECRET_KEY="${process.env.ATOMS_E2E_OAUTH_CLIENT_SECRET}" NEXT_PUBLIC_CALCOM_API_URL="${process.env.ATOMS_E2E_API_URL}" VITE_BOOKER_EMBED_OAUTH_CLIENT_ID="${process.env.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED}" VITE_BOOKER_EMBED_API_URL="${process.env.ATOMS_E2E_API_URL}" ORGANIZATION_ID=${process.env.ATOMS_E2E_ORG_ID} yarn dev:e2e`
: `rm -f prisma/dev.db && yarn prisma db push && yarn dev:e2e`,
? `yarn workspace @calcom/atoms dev-on && yarn workspace @calcom/atoms build && yarn db:generate:test && yarn db:reset:test && yarn dev:e2e`
: `yarn db:generate:test && yarn db:reset:test && yarn dev:e2e`,
url: "http://localhost:4322",
timeout: 600_000,
reuseExistingServer: !process.env.CI,
...(process.env.CI
? {
env: {
NEXT_PUBLIC_IS_E2E: "1",
NODE_ENV: "test",
NEXT_PUBLIC_X_CAL_ID: process.env.ATOMS_E2E_OAUTH_CLIENT_ID ?? "",
X_CAL_SECRET_KEY: process.env.ATOMS_E2E_OAUTH_CLIENT_SECRET ?? "",
NEXT_PUBLIC_CALCOM_API_URL: process.env.ATOMS_E2E_API_URL ?? "",
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: process.env.ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED ?? "",
VITE_BOOKER_EMBED_API_URL: process.env.ATOMS_E2E_API_URL ?? "",
ORGANIZATION_ID: String(process.env.ATOMS_E2E_ORG_ID ?? ""),
},
}
: {}),
},
});
19 changes: 19 additions & 0 deletions packages/platform/examples/base/prisma/schema.test.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
generator testClient {
provider = "prisma-client-js"
output = "../node_modules/.prisma/test-client"
}
datasource testDb {
provider = "sqlite"
url = "file:./test.db"
}
model User {
id Int @id @default(autoincrement())
email String @unique(map: "TestUser_email_key")
name String?
calcomUserId Int? @unique(map: "TestUser_calcomUserId_key")
calcomUsername String? @unique(map: "TestUser_calcomUsername_key")
refreshToken String? @unique(map: "TestUser_refreshToken_key")
accessToken String? @unique(map: "TestUser_accessToken_key")
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
}
9 changes: 5 additions & 4 deletions packages/platform/examples/base/src/lib/prismaClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// prisma client of example app
// using local prisma db, not related to the cal.com monorepo prisma client
// eslint-disable-next-line
import { PrismaClient } from "@prisma/client";
const isE2E = process.env.NEXT_PUBLIC_IS_E2E === "1";

const { PrismaClient } = isE2E
? require("../../node_modules/.prisma/test-client")
: require("@prisma/client");

const prismaClientSingleton = () => {
return new PrismaClient();
Expand Down
Loading
Loading