Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Loading