Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
87 changes: 65 additions & 22 deletions packages/features/shell/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,31 @@ export type SideBarProps = {
export function SideBarContainer({ bannersHeight, isPlatformUser = false }: SideBarContainerProps) {
const { status, data } = useSession();

// Make sure that Sidebar is rendered optimistically so that a refresh of pages when logged in have SideBar from the beginning.
// This improves the experience of refresh on app store pages(when logged in) which are SSG.
// Though when logged out, app store pages would temporarily show SideBar until session status is confirmed.
// Render nothing once we know the user isn't authenticated.
if (status !== "loading" && status !== "authenticated") return null;
return <SideBar isPlatformUser={isPlatformUser} bannersHeight={bannersHeight} user={data?.user} />;
}

// Build a safe absolute public page URL, or empty string if we can't.
const buildPublicPageUrl = (user?: UserAuth | null): string => {
const base =
getBookerBaseUrlSync(user?.org?.slug ?? null) ||
process.env.NEXT_PUBLIC_WEBAPP_URL ||
process.env.NEXTAUTH_URL ||
"";
const username = user?.orgAwareUsername;
if (!base || !username) return "";
return `${String(base).replace(/\/+$/, "")}/${username}`;
};

export function SideBar({ bannersHeight, user }: SideBarProps) {
const session = useSession();
const { t, isLocaleReady } = useLocale();
const pathname = usePathname();
const isPlatformPages = pathname?.startsWith("/settings/platform");
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;

const publicPageUrl = `${getBookerBaseUrlSync(user?.org?.slug ?? null)}/${user?.orgAwareUsername}`;
const publicPageUrl = buildPublicPageUrl(user);

const bottomNavItems = useBottomNavItems({
publicPageUrl,
Expand Down Expand Up @@ -134,10 +144,12 @@ export function SideBar({ bannersHeight, user }: SideBarProps) {
<KBarTrigger />
</div>
</header>

{/* logo icon for tablet */}
<Link href="/event-types" className="text-center md:inline lg:hidden">
<Logo small icon />
</Link>

<Navigation isPlatformNavigation={isPlatformPages} />
</div>

Expand All @@ -146,21 +158,13 @@ export function SideBar({ bannersHeight, user }: SideBarProps) {
<div className="overflow-hidden">
<Tips />
</div>
{bottomNavItems.map((item, index) => (
<Tooltip side="right" content={t(item.name)} className="lg:hidden" key={item.name}>
<ButtonOrLink
id={item.name}
href={item.href || undefined}
aria-label={t(item.name)}
target={item.target}
className={classNames(
"text-left",
"[&[aria-current='page']]:bg-emphasis text-default justify-right group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
"[&[aria-current='page']]:text-emphasis mt-0.5 w-full text-sm",
isLocaleReady ? "hover:bg-emphasis hover:text-emphasis" : "",
index === 0 && "mt-3"
)}
onClick={item.onClick}>

{bottomNavItems.map((item, index) => {
const isActionOnly = !item.href;
const isInternal = !!item.href && item.href.startsWith("/");

const content = (
<>
{!!item.icon && (
<Icon
name={item.isLoading ? "rotate-cw" : item.icon}
Expand All @@ -179,9 +183,48 @@ export function SideBar({ bannersHeight, user }: SideBarProps) {
) : (
<SkeletonText className="h-[20px] w-full" />
)}
</ButtonOrLink>
</Tooltip>
))}
</>
);

const commonClassName = classNames(
"text-left",
"[&[aria-current='page']]:bg-emphasis text-default justify-right group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
"[&[aria-current='page']]:text-emphasis mt-0.5 w-full text-sm",
isLocaleReady ? "hover:bg-emphasis hover:text-emphasis" : "",
index === 0 && "mt-3"
);

return (
<Tooltip side="right" content={t(item.name)} className="lg:hidden" key={item.name}>
{isActionOnly ? (
<button
id={item.name}
type="button"
onClick={item.onClick}
className={commonClassName}
aria-label={t(item.name)}
disabled={item.isLoading}>
{content}
</button>
) : isInternal ? (
<Link href={item.href!} className={commonClassName} aria-label={t(item.name)}>
{content}
</Link>
) : (
<ButtonOrLink
id={item.name}
href={item.href!} // external/absolute only
target={item.target === "__blank" ? "_blank" : item.target}
onClick={item.onClick}
className={commonClassName}
aria-label={t(item.name)}>
{content}
</ButtonOrLink>
)}
</Tooltip>
);
})}

{!IS_VISUAL_REGRESSION_TESTING && <Credits />}
</div>
)}
Expand Down
69 changes: 50 additions & 19 deletions packages/features/shell/useBottomNavItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,23 @@ import { showToast } from "@calcom/ui/components/toast";
import { type NavigationItemType } from "./navigation/NavigationItem";

type BottomNavItemsProps = {
publicPageUrl: string;
publicPageUrl: string; // can be empty/relative; we’ll normalize
isAdmin: boolean;
user: UserAuth | null | undefined;
};

// Resolve any href (absolute or relative) to an absolute URL for SSR safety.
// Returns undefined if it can’t be resolved.
const withBase = (u?: string): string | undefined => {
if (!u) return undefined;
const base = process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_WEBAPP_URL || "http://localhost:3000";
try {
return new URL(u, base).toString();
} catch {
return undefined;
}
};

export function useBottomNavItems({
publicPageUrl,
isAdmin,
Expand All @@ -33,12 +45,18 @@ export function useBottomNavItems({
},
});

const safePublicHref = withBase(publicPageUrl);
const safeReferHref = withBase("/refer");
const safeImpersonationHref = withBase("/settings/admin/impersonation");
const safeSettingsHref = withBase(
user?.org ? "/settings/organizations/profile" : "/settings/my-account/profile"
);

return [
// Render above to prevent layout shift as much as possible
// Action-only (no href)
isTrial
? {
name: "skip_trial",
href: "",
isLoading: skipTeamTrialsMutation.isPending,
icon: "clock",
onClick: (e: { preventDefault: () => void }) => {
Expand All @@ -47,40 +65,53 @@ export function useBottomNavItems({
},
}
: null,
{
name: "view_public_page",
href: publicPageUrl,
icon: "external-link",
target: "__blank",
},

// External public page link, only if we have a resolvable absolute URL
safePublicHref
? {
name: "view_public_page",
href: safePublicHref,
icon: "external-link",
target: "_blank",
}
: null,

// Action-only (no href)
{
name: "copy_public_page_link",
href: "",
icon: "copy",
onClick: (e: { preventDefault: () => void }) => {
e.preventDefault();
navigator.clipboard.writeText(publicPageUrl);
showToast(t("link_copied"), "success");
// Prefer resolved; fall back to raw input if clipboard is allowed
const toCopy = safePublicHref ?? publicPageUrl ?? "";
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText && toCopy) {
navigator.clipboard.writeText(toCopy);
showToast(t("link_copied"), "success");
} else {
showToast(t("something_went_wrong"), "error");
}
},
icon: "copy",
},

IS_DUB_REFERRALS_ENABLED
? {
? safeReferHref && {
name: "referral_text",
href: "/refer",
href: safeReferHref,
icon: "gift",
}
: null,

isAdmin
? {
? safeImpersonationHref && {
name: "impersonation",
href: "/settings/admin/impersonation",
href: safeImpersonationHref,
icon: "lock",
}
: null,
{

safeSettingsHref && {
name: "settings",
href: user?.org ? `/settings/organizations/profile` : "/settings/my-account/profile",
href: safeSettingsHref,
icon: "settings",
},
].filter(Boolean) as NavigationItemType[];
Expand Down
Loading
Loading