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
2 changes: 2 additions & 0 deletions .changeset/beige-lilies-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
12 changes: 6 additions & 6 deletions .claude/rules/e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ A single test runner used by both `bun run test` and `bun run test:e2e`. Each te
Each fixture directory contains:

- Framework source files (scaffolded by `config.scaffoldCmd`)
- A `.test.ts` file that exports a `config: FixtureConfig` and calls `runFixtureTest()` and `runBrowserTest()`
- A `.test.ts` file that exports a `config: FixtureConfig` and calls `runFixtureTests()` and `runBrowserTests()`

### FixtureConfig

Expand All @@ -102,11 +102,11 @@ Defined in `test/e2e/lib/types.ts`:
5. Parse `.env` / `.env.local` for publishable and secret keys (uses `detectPublishableKeyName` / `detectSecretKeyName` from CLI source)
6. `bun install`

### Build + typecheck test (`runFixtureTest`)
### Build + typecheck test (`runFixtureTests`)

Runs the framework build command, then `tsc --noEmit`. If the fixture has a `typecheck` script in its `package.json`, that's used instead of bare `tsc` (handles React Router's `react-router typegen`).

### Browser auth test (`runBrowserTest`)
### Browser auth test (`runBrowserTests`)

1. Creates a disposable test user via `clerk api /users -X POST` (uses `+clerk_test` email suffix for OTP bypass)
2. Starts the framework's dev server on a dynamic port
Expand All @@ -131,19 +131,19 @@ In CI, use `bunx playwright install chromium --with-deps` to include system-leve

Fixture files run in parallel (concurrency controlled by the runner, defaults to CPU count). Each fixture uses an isolated temp directory and `CLERK_CONFIG_DIR`, so there is no shared mutable state. Do not use `test.concurrent` within individual fixture files.

Within each test file, `useFixture()` runs `setupFixture()` once in `beforeAll` and shares the result with both the build test and browser test. This avoids duplicating the expensive setup.
Within each test file, `createFixtureHarness()` runs `setupFixture()` once in `beforeAll` and shares the result with both the build test and browser test. This avoids duplicating the expensive setup.

## Adding a new fixture

1. Create `test/e2e/fixtures/<name>/`
2. Scaffold the framework manually or via `bun run e2e:refresh-fixtures`
3. Add a `<name>.test.ts` exporting `config: FixtureConfig` and calling `runFixtureTest()` and `runBrowserTest()`
3. Add a `<name>.test.ts` exporting `config: FixtureConfig` and calling `runFixtureTests()` and `runBrowserTests()`
4. Add a `README.md` in the fixture directory describing the project

Helper functions are in `test/e2e/lib/`:

- `fixture-setup.ts` - `setupFixture`
- `fixture-test.ts` - `useFixture`, `runFixtureTest`, `runBrowserTest`
- `fixture-test.ts` - `createFixtureHarness`, `runFixtureTests`, `runFileExistsTest`, `runBrowserTests`
- `dev-server.ts` - `startDevServer` (allocates a port internally and retries on collision), `killDevServer`, `buildDevCommand`
- `test-user.ts` - `createTestUser`, `deleteTestUser`
- `logger.ts` - `log`, `debug` (shared logging; set `CLERK_E2E_DEBUG=1` for verbose output)
Expand Down
4 changes: 4 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"ignorePatterns": ["test/e2e/fixtures/**"]
}
5 changes: 4 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"rules": {
"unicorn/no-process-exit": "error",
"no-console": "error"
},
"ignorePatterns": ["test/e2e/fixtures/**"],
"overrides": [
{
"files": [
Expand All @@ -18,7 +20,8 @@
"files": [
"scripts/**",
"packages/cli-core/src/**/*.test.ts",
"packages/cli-core/src/test/**"
"packages/cli-core/src/test/**",
"test/e2e/**"
],
"rules": {
"no-console": "off"
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ bunx playwright install chromium

bun run test:e2e:op # Run all E2E tests (secrets from 1Password)
bun run test:e2e:op -- --filter react # Run only tests matching "react"
bun run test:e2e:op -- --debug # Verbose helper logging (sets CLERK_E2E_DEBUG=1)
bun run test:e2e:op -- --debug # Force serial execution for parsing logs (sets CLERK_E2E_DEBUG=1)
bun run test:e2e:op -- --har # Capture HAR files to test/e2e/.har for network debugging
bun run test:e2e:op -- --har-dir ./out # Capture HAR files to a custom directory
bun run e2e:refresh-fixtures # Re-scaffold fixture projects from upstream CLIs
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
"scripts": {
"build": "bun run --filter @clerk/cli-core build",
"dev": "bun run --cwd packages/cli-core dev",
"test": "bun run scripts/run-tests.ts --pattern 'packages/cli-core/src/**/*.test.ts' --pattern 'scripts/**/*.test.ts'",
"test:e2e": "bun run scripts/run-tests.ts --pattern 'test/e2e/*.test.ts' --retries 1",
"test": "bun test 'packages/cli-core/src/' 'scripts/' --parallel --only-failures",
"test:e2e": "bun test 'test/e2e/' --retry 1 --parallel --only-failures",
"test:e2e:op": "bun run scripts/run-e2e-op.ts",
"e2e:refresh-fixtures": "bun run scripts/refresh-e2e-fixtures.ts",
"typecheck": "bun run --filter './packages/*' typecheck && tsc --noEmit -p scripts/tsconfig.json",
"lint": "bun run --filter './packages/*' lint && oxlint -c .oxlintrc.json scripts/",
"format": "bun run --filter './packages/*' format && oxfmt --write scripts/",
"format:check": "bun run --filter './packages/*' format:check && oxfmt --check scripts/",
"typecheck": "bun run --filter './packages/*' typecheck && tsc --noEmit -p scripts/tsconfig.json && tsc --noEmit -p test/e2e/tsconfig.json",
"lint": "bun run --filter './packages/*' lint && oxlint -c .oxlintrc.json scripts/ test/e2e/",
"format": "bun run --filter './packages/*' format && oxfmt --write scripts/ test/e2e/",
"format:check": "bun run --filter './packages/*' format:check && oxfmt --check scripts/ test/e2e/",
"check:patches": "bun run scripts/check-patches.ts",
"build:compile": "bun run --filter @clerk/cli-core build:compile",
"version-packages": "bun changeset version",
Expand Down Expand Up @@ -42,10 +42,10 @@
},
"nano-staged": {
"*.{ts,tsx,js,jsx}": [
"oxfmt --write",
"oxlint -c .oxlintrc.json"
"oxfmt --write --no-error-on-unmatched-pattern",
"oxlint -c .oxlintrc.json --no-error-on-unmatched-pattern"
],
"*.{md,json,css}": "oxfmt --write"
"*.{md,json,css}": "oxfmt --write --no-error-on-unmatched-pattern"
},
"engines": {
"bun": ">=1.3.10"
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"typecheck": "tsc --noEmit -p tsconfig.json",
"lint": "oxlint src/",
"format": "oxfmt --write src/",
"format:check": "oxfmt --check src/"
"format:check": "oxfmt --check src/",
"test": "bun test src/ --parallel"
},
"dependencies": {
"@clerk/cli-extras": "workspace:*",
Expand Down
21 changes: 20 additions & 1 deletion packages/cli-core/src/test/integration/lib/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,24 @@ mock.module(

// ── Real config module ───────────────────────────────────────────────────────

export const { _setConfigDir, readConfig, setProfile } = await import("../../../lib/config.ts");
type ConfigModule = typeof import("../../../lib/config.ts");

let configModulePromise: Promise<ConfigModule> | null = null;

function getConfigModule(): Promise<ConfigModule> {
configModulePromise ??= import("../../../lib/config.ts");
return configModulePromise;
}

export async function readConfig(): ReturnType<ConfigModule["readConfig"]> {
return (await getConfigModule()).readConfig();
}

export async function setProfile(
...args: Parameters<ConfigModule["setProfile"]>
): ReturnType<ConfigModule["setProfile"]> {
return (await getConfigModule()).setProfile(...args);
}

// ── Mock data ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -526,6 +543,7 @@ function setEnv(key: string, value: string) {
*/
export async function setupTest(): Promise<TestHarness> {
const tempDir = await mkdtemp(join(tmpdir(), "clerk-integration-"));
const { _setConfigDir } = await getConfigModule();
_setConfigDir(tempDir);
process.cwd = () => tempDir;
setEnv("CLERK_PLATFORM_API_KEY", "test_platform_key");
Expand Down Expand Up @@ -558,6 +576,7 @@ export async function setupTest(): Promise<TestHarness> {
* temporary directory.
*/
export async function teardownTest(harness: TestHarness): Promise<void> {
const { _setConfigDir } = await getConfigModule();
currentHarness = null;
assertPromptQueuesEmpty();
http.assertRoutesConsumed();
Expand Down
134 changes: 134 additions & 0 deletions scripts/lib/fixture-deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, expect, test } from "bun:test";
import {
applyPackageJsonOverrides,
assertPinnedDependencyRanges,
resolveDependencySpecsToExactVersions,
validatePinnedDependencyRanges,
} from "./fixture-deps.ts";

describe("applyPackageJsonOverrides", () => {
test("merges dependency overrides into package.json", () => {
const pkg: {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
} = {
dependencies: {
existing: "^1",
},
};

applyPackageJsonOverrides(pkg, {
dependencies: {
added: "^2",
},
devDependencies: {
dev: "^3",
},
});

expect(pkg.dependencies).toEqual({
existing: "^1",
added: "^2",
});
expect(pkg.devDependencies).toEqual({
dev: "^3",
});
});
});

describe("validatePinnedDependencyRanges", () => {
test("allows satisfying generated dependencies without changing package.json", () => {
const pkg = {
dependencies: {
"fixture-framework": "1.2.3",
react: "^18",
},
};

const warnings = validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" });

expect(warnings).toEqual([]);
expect(pkg.dependencies["fixture-framework"]).toBe("1.2.3");
});

test("warns and keeps generated dependency when it falls outside the configured range", () => {
const pkg = {
dependencies: {
"fixture-framework": "2.0.0",
},
};

const warnings = validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" });

expect(pkg.dependencies["fixture-framework"]).toBe("2.0.0");
expect(warnings).toEqual([
'fixture-framework generated version "2.0.0" does not satisfy pinned range "^1"',
]);
});
});

describe("assertPinnedDependencyRanges", () => {
test("throws when pinned dependency validation fails", () => {
const pkg = {
dependencies: {
"fixture-framework": "2.0.0",
},
};

expect(() =>
assertPinnedDependencyRanges(pkg, { "fixture-framework": "^1" }, "fixture-name"),
).toThrow(
'Pinned dependency validation failed for fixture-name:\n - fixture-framework generated version "2.0.0" does not satisfy pinned range "^1"',
);
});
});

describe("resolveDependencySpecsToExactVersions", () => {
test("rewrites generated dependency ranges to exact versions", async () => {
const pkg = {
dependencies: {
"@clerk/react": "latest",
react: "^19.0.0",
"already-exact": "1.2.3",
},
devDependencies: {
typescript: "~5.9.0",
},
};
const resolved: string[] = [];
const versions: Record<string, string> = {
"react@^19.0.0": "19.2.6",
"typescript@~5.9.0": "5.9.3",
};

await resolveDependencySpecsToExactVersions(pkg, async (name, spec) => {
resolved.push(`${name}@${spec}`);
return versions[`${name}@${spec}`]!;
});

expect(pkg).toEqual({
dependencies: {
"@clerk/react": "latest",
react: "19.2.6",
"already-exact": "1.2.3",
},
devDependencies: {
typescript: "5.9.3",
},
});
expect(resolved).toEqual(["react@^19.0.0", "typescript@~5.9.0"]);
});

test("resolves pinned dependency ranges to exact satisfying versions", async () => {
const pkg = {
dependencies: {
"fixture-framework": "^1",
},
};

await resolveDependencySpecsToExactVersions(pkg, async () => "1.2.3");

expect(pkg.dependencies["fixture-framework"]).toBe("1.2.3");
expect(validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" })).toEqual([]);
});
});
Loading