Skip to content

Commit 2ca4be0

Browse files
committed
test(e2e): pin fixture dependencies, migrate to npm, and overhaul fixture lifecycle
Pin every E2E fixture project's dependencies and check in package-lock.json files so fixture installs are reproducible and skip scaffolding the upstream templates from scratch on each run. Switch fixtures from bun-based installs to npm to match what users would do on a freshly scaffolded app. Replace the useFixture helper with createGetFixture, returning { fixture, users } so each test owns its own user lifecycle and tracks created users for explicit cleanup. Add scripts/lib/fixture-deps for the shared pinning logic plus tests. Migrate test infrastructure off scripts/run-tests.ts to native bun test --parallel for both unit and E2E pipelines, and fold test/e2e into the lint, format, and typecheck commands.
1 parent 58a431d commit 2ca4be0

55 files changed

Lines changed: 33393 additions & 639 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/e2e.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ In CI, use `bunx playwright install chromium --with-deps` to include system-leve
131131

132132
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.
133133

134-
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.
134+
Within each test file, `createGetFixture()` runs `setupFixture()` once in `beforeAll` and shares the result with both the build test and browser test. This avoids duplicating the expensive setup.
135135

136136
## Adding a new fixture
137137

@@ -143,7 +143,7 @@ Within each test file, `useFixture()` runs `setupFixture()` once in `beforeAll`
143143
Helper functions are in `test/e2e/lib/`:
144144

145145
- `fixture-setup.ts` - `setupFixture`
146-
- `fixture-test.ts` - `useFixture`, `runFixtureTest`, `runBrowserTest`
146+
- `fixture-test.ts` - `createGetFixture`, `runFixtureTest`, `runFileExistsTest`, `runBrowserTest`
147147
- `dev-server.ts` - `startDevServer` (allocates a port internally and retries on collision), `killDevServer`, `buildDevCommand`
148148
- `test-user.ts` - `createTestUser`, `deleteTestUser`
149149
- `logger.ts` - `log`, `debug` (shared logging; set `CLERK_E2E_DEBUG=1` for verbose output)

.oxfmtrc.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "./node_modules/oxfmt/configuration_schema.json",
3+
"ignorePatterns": ["test/e2e/fixtures/**"]
4+
}

.oxlintrc.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{
2+
"$schema": "./node_modules/oxlint/configuration_schema.json",
23
"rules": {
34
"unicorn/no-process-exit": "error",
45
"no-console": "error"
56
},
7+
"ignorePatterns": ["test/e2e/fixtures/**"],
68
"overrides": [
79
{
810
"files": [
@@ -18,7 +20,8 @@
1820
"files": [
1921
"scripts/**",
2022
"packages/cli-core/src/**/*.test.ts",
21-
"packages/cli-core/src/test/**"
23+
"packages/cli-core/src/test/**",
24+
"test/e2e/**"
2225
],
2326
"rules": {
2427
"no-console": "off"

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ bunx playwright install chromium
8888

8989
bun run test:e2e:op # Run all E2E tests (secrets from 1Password)
9090
bun run test:e2e:op -- --filter react # Run only tests matching "react"
91-
bun run test:e2e:op -- --debug # Verbose helper logging (sets CLERK_E2E_DEBUG=1)
91+
bun run test:e2e:op -- --debug # Force serial execution for parsing logs (sets CLERK_E2E_DEBUG=1)
9292
bun run test:e2e:op -- --har # Capture HAR files to test/e2e/.har for network debugging
9393
bun run test:e2e:op -- --har-dir ./out # Capture HAR files to a custom directory
9494
bun run e2e:refresh-fixtures # Re-scaffold fixture projects from upstream CLIs

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
"scripts": {
88
"build": "bun run --filter @clerk/cli-core build",
99
"dev": "bun run --cwd packages/cli-core dev",
10-
"test": "bun run scripts/run-tests.ts --pattern 'packages/cli-core/src/**/*.test.ts' --pattern 'scripts/**/*.test.ts'",
11-
"test:e2e": "bun run scripts/run-tests.ts --pattern 'test/e2e/*.test.ts' --retries 1",
10+
"test": "bun test 'packages/cli-core/src/' 'scripts/' --parallel --only-failures",
11+
"test:e2e": "bun test 'test/e2e/' --retry 1 --parallel --only-failures",
1212
"test:e2e:op": "bun run scripts/run-e2e-op.ts",
1313
"e2e:refresh-fixtures": "bun run scripts/refresh-e2e-fixtures.ts",
14-
"typecheck": "bun run --filter './packages/*' typecheck && tsc --noEmit -p scripts/tsconfig.json",
15-
"lint": "bun run --filter './packages/*' lint && oxlint -c .oxlintrc.json scripts/",
16-
"format": "bun run --filter './packages/*' format && oxfmt --write scripts/",
17-
"format:check": "bun run --filter './packages/*' format:check && oxfmt --check scripts/",
14+
"typecheck": "bun run --filter './packages/*' typecheck && tsc --noEmit -p scripts/tsconfig.json && tsc --noEmit -p test/e2e/tsconfig.json",
15+
"lint": "bun run --filter './packages/*' lint && oxlint -c .oxlintrc.json scripts/ test/e2e/",
16+
"format": "bun run --filter './packages/*' format && oxfmt --write scripts/ test/e2e/",
17+
"format:check": "bun run --filter './packages/*' format:check && oxfmt --check scripts/ test/e2e/",
1818
"check:patches": "bun run scripts/check-patches.ts",
1919
"build:compile": "bun run --filter @clerk/cli-core build:compile",
2020
"version-packages": "bun changeset version",

packages/cli-core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"typecheck": "tsc --noEmit -p tsconfig.json",
1414
"lint": "oxlint src/",
1515
"format": "oxfmt --write src/",
16-
"format:check": "oxfmt --check src/"
16+
"format:check": "oxfmt --check src/",
17+
"test": "bun test src/ --parallel"
1718
},
1819
"dependencies": {
1920
"@clerk/cli-extras": "workspace:*",

scripts/lib/fixture-deps.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { describe, expect, test } from "bun:test";
2+
import {
3+
applyPackageJsonOverrides,
4+
assertPinnedDependencyRanges,
5+
resolveDependencySpecsToExactVersions,
6+
validatePinnedDependencyRanges,
7+
} from "./fixture-deps.ts";
8+
9+
describe("applyPackageJsonOverrides", () => {
10+
test("merges dependency overrides into package.json", () => {
11+
const pkg: {
12+
dependencies?: Record<string, string>;
13+
devDependencies?: Record<string, string>;
14+
} = {
15+
dependencies: {
16+
existing: "^1",
17+
},
18+
};
19+
20+
applyPackageJsonOverrides(pkg, {
21+
dependencies: {
22+
added: "^2",
23+
},
24+
devDependencies: {
25+
dev: "^3",
26+
},
27+
});
28+
29+
expect(pkg.dependencies).toEqual({
30+
existing: "^1",
31+
added: "^2",
32+
});
33+
expect(pkg.devDependencies).toEqual({
34+
dev: "^3",
35+
});
36+
});
37+
});
38+
39+
describe("validatePinnedDependencyRanges", () => {
40+
test("allows satisfying generated dependencies without changing package.json", () => {
41+
const pkg = {
42+
dependencies: {
43+
"fixture-framework": "1.2.3",
44+
react: "^18",
45+
},
46+
};
47+
48+
const warnings = validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" });
49+
50+
expect(warnings).toEqual([]);
51+
expect(pkg.dependencies["fixture-framework"]).toBe("1.2.3");
52+
});
53+
54+
test("warns and keeps generated dependency when it falls outside the configured range", () => {
55+
const pkg = {
56+
dependencies: {
57+
"fixture-framework": "2.0.0",
58+
},
59+
};
60+
61+
const warnings = validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" });
62+
63+
expect(pkg.dependencies["fixture-framework"]).toBe("2.0.0");
64+
expect(warnings).toEqual([
65+
'fixture-framework generated version "2.0.0" does not satisfy pinned range "^1"',
66+
]);
67+
});
68+
});
69+
70+
describe("assertPinnedDependencyRanges", () => {
71+
test("throws when pinned dependency validation fails", () => {
72+
const pkg = {
73+
dependencies: {
74+
"fixture-framework": "2.0.0",
75+
},
76+
};
77+
78+
expect(() =>
79+
assertPinnedDependencyRanges(pkg, { "fixture-framework": "^1" }, "fixture-name"),
80+
).toThrow(
81+
'Pinned dependency validation failed for fixture-name:\n - fixture-framework generated version "2.0.0" does not satisfy pinned range "^1"',
82+
);
83+
});
84+
});
85+
86+
describe("resolveDependencySpecsToExactVersions", () => {
87+
test("rewrites generated dependency ranges to exact versions", async () => {
88+
const pkg = {
89+
dependencies: {
90+
"@clerk/react": "latest",
91+
react: "^19.0.0",
92+
"already-exact": "1.2.3",
93+
},
94+
devDependencies: {
95+
typescript: "~5.9.0",
96+
},
97+
};
98+
const resolved: string[] = [];
99+
const versions: Record<string, string> = {
100+
"react@^19.0.0": "19.2.6",
101+
"typescript@~5.9.0": "5.9.3",
102+
};
103+
104+
await resolveDependencySpecsToExactVersions(pkg, async (name, spec) => {
105+
resolved.push(`${name}@${spec}`);
106+
return versions[`${name}@${spec}`]!;
107+
});
108+
109+
expect(pkg).toEqual({
110+
dependencies: {
111+
"@clerk/react": "latest",
112+
react: "19.2.6",
113+
"already-exact": "1.2.3",
114+
},
115+
devDependencies: {
116+
typescript: "5.9.3",
117+
},
118+
});
119+
expect(resolved).toEqual(["react@^19.0.0", "typescript@~5.9.0"]);
120+
});
121+
122+
test("resolves pinned dependency ranges to exact satisfying versions", async () => {
123+
const pkg = {
124+
dependencies: {
125+
"fixture-framework": "^1",
126+
},
127+
};
128+
129+
await resolveDependencySpecsToExactVersions(pkg, async () => "1.2.3");
130+
131+
expect(pkg.dependencies["fixture-framework"]).toBe("1.2.3");
132+
expect(validatePinnedDependencyRanges(pkg, { "fixture-framework": "^1" })).toEqual([]);
133+
});
134+
});

scripts/lib/fixture-deps.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import semver from "semver";
2+
3+
type PackageJson = {
4+
dependencies?: Record<string, string>;
5+
devDependencies?: Record<string, string>;
6+
};
7+
8+
type DependencyField = "dependencies" | "devDependencies";
9+
10+
export type DependencyVersionResolver = (name: string, spec: string) => string | Promise<string>;
11+
12+
export type PackageJsonOverrides = {
13+
dependencies?: Record<string, string>;
14+
devDependencies?: Record<string, string>;
15+
};
16+
17+
const DEPENDENCY_FIELDS: DependencyField[] = ["dependencies", "devDependencies"];
18+
const EXACT_VERSION_EXCLUDED_PACKAGE_SCOPES = ["@clerk/"];
19+
20+
export function applyPackageJsonOverrides(
21+
pkg: PackageJson,
22+
overrides: PackageJsonOverrides | undefined,
23+
): void {
24+
if (!overrides) return;
25+
26+
if (overrides.dependencies) {
27+
pkg.dependencies = { ...pkg.dependencies, ...overrides.dependencies };
28+
}
29+
30+
if (overrides.devDependencies) {
31+
pkg.devDependencies = { ...pkg.devDependencies, ...overrides.devDependencies };
32+
}
33+
}
34+
35+
export async function resolveDependencySpecsToExactVersions(
36+
pkg: PackageJson,
37+
resolveVersion: DependencyVersionResolver,
38+
): Promise<void> {
39+
for (const field of DEPENDENCY_FIELDS) {
40+
const deps = pkg[field];
41+
if (!deps) continue;
42+
43+
for (const [name, spec] of Object.entries(deps)) {
44+
if (EXACT_VERSION_EXCLUDED_PACKAGE_SCOPES.some((scope) => name.startsWith(scope))) {
45+
continue;
46+
}
47+
48+
const exact = semver.valid(spec);
49+
if (exact) {
50+
deps[name] = exact;
51+
continue;
52+
}
53+
54+
const resolved = await resolveVersion(name, spec);
55+
const resolvedExact = semver.valid(resolved);
56+
if (!resolvedExact) {
57+
throw new Error(`${name}@${spec} resolved to non-exact version "${resolved}"`);
58+
}
59+
60+
deps[name] = resolvedExact;
61+
}
62+
}
63+
}
64+
65+
function isSpecWithinRange(spec: string, range: string): boolean {
66+
if (!semver.validRange(range)) return false;
67+
68+
const exact = semver.valid(spec);
69+
if (exact) return semver.satisfies(exact, range);
70+
71+
const specRange = semver.validRange(spec);
72+
return Boolean(specRange && semver.subset(specRange, range));
73+
}
74+
75+
export function validatePinnedDependencyRanges(
76+
pkg: PackageJson,
77+
pinnedDependencyRanges: Record<string, string> | undefined,
78+
): string[] {
79+
if (!pinnedDependencyRanges) return [];
80+
81+
const warnings: string[] = [];
82+
83+
for (const [dep, range] of Object.entries(pinnedDependencyRanges)) {
84+
const deps = pkg.dependencies;
85+
const devDeps = pkg.devDependencies;
86+
const target = deps?.[dep] !== undefined ? deps : devDeps?.[dep] !== undefined ? devDeps : null;
87+
88+
if (!target) {
89+
warnings.push(`${dep} was not generated, so pinned range "${range}" was not applied`);
90+
continue;
91+
}
92+
93+
const generatedSpec = target[dep]!;
94+
if (!isSpecWithinRange(generatedSpec, range)) {
95+
warnings.push(
96+
`${dep} generated version "${generatedSpec}" does not satisfy pinned range "${range}"`,
97+
);
98+
continue;
99+
}
100+
}
101+
102+
return warnings;
103+
}
104+
105+
export function assertPinnedDependencyRanges(
106+
pkg: PackageJson,
107+
pinnedDependencyRanges: Record<string, string> | undefined,
108+
fixtureName: string,
109+
): void {
110+
const errors = validatePinnedDependencyRanges(pkg, pinnedDependencyRanges);
111+
if (errors.length === 0) return;
112+
113+
throw new Error(
114+
[
115+
`Pinned dependency validation failed for ${fixtureName}:`,
116+
...errors.map((error) => ` - ${error}`),
117+
].join("\n"),
118+
);
119+
}

0 commit comments

Comments
 (0)