Skip to content

Commit 7752d60

Browse files
authored
chore(tests): Added test variant flag (#18458)
I have been running a lot of Next.js tests and needed to run specific variants against my changes, so I made these changes to our test runner script and thought to share it with everyone. ``` yarn test:run <app-name> --variant <variant-name> ``` Closes #18459 (added automatically)
1 parent d4301fd commit 7752d60

File tree

2 files changed

+166
-7
lines changed

2 files changed

+166
-7
lines changed

dev-packages/e2e-tests/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,37 @@ Or run only a single E2E test app:
2525
yarn test:run <app-name>
2626
```
2727

28+
Or you can run a single E2E test app with a specific variant:
29+
30+
```bash
31+
yarn test:run <app-name> --variant <variant-name>
32+
```
33+
34+
Variant name matching is case-insensitive and partial. For example, `--variant 13` will match `nextjs-pages-dir (next@13)` if a matching variant is present in the test app's `package.json`.
35+
36+
For example, if you have the following variants in your test app's `package.json`:
37+
38+
```json
39+
"sentryTest": {
40+
"variants": [
41+
{
42+
"build-command": "pnpm test:build-13",
43+
"label": "nextjs-pages-dir (next@13)"
44+
},
45+
{
46+
"build-command": "pnpm test:build-13-canary",
47+
"label": "nextjs-pages-dir (next@13-canary)"
48+
},
49+
{
50+
"build-command": "pnpm test:build-15",
51+
"label": "nextjs-pages-dir (next@15)"
52+
}
53+
]
54+
}
55+
```
56+
57+
If you run `yarn test:run nextjs-pages-dir --variant 13`, it will match against the very first matching variant, which is `nextjs-pages-dir (next@13)`. If you need to target the second variant in the example, you need to be more specific and use `--variant 13-canary`.
58+
2859
## How they work
2960

3061
Before running any tests we launch a fake test registry (in our case [Verdaccio](https://verdaccio.org/docs/e2e/)), we

dev-packages/e2e-tests/run.ts

Lines changed: 135 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
/* eslint-disable no-console */
22
import { spawn } from 'child_process';
33
import * as dotenv from 'dotenv';
4-
import { mkdtemp, rm } from 'fs/promises';
4+
import { mkdtemp, readFile, rm } from 'fs/promises';
55
import { sync as globSync } from 'glob';
66
import { tmpdir } from 'os';
77
import { join, resolve } from 'path';
88
import { copyToTemp } from './lib/copyToTemp';
99
import { registrySetup } from './registrySetup';
1010

11+
interface SentryTestVariant {
12+
'build-command': string;
13+
'assert-command'?: string;
14+
label?: string;
15+
}
16+
17+
interface PackageJson {
18+
sentryTest?: {
19+
variants?: SentryTestVariant[];
20+
optionalVariants?: SentryTestVariant[];
21+
};
22+
}
23+
1124
const DEFAULT_DSN = 'https://username@domain/123';
1225
const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks';
1326
const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests';
@@ -58,14 +71,100 @@ function asyncExec(
5871
});
5972
}
6073

74+
function findMatchingVariant(variants: SentryTestVariant[], variantLabel: string): SentryTestVariant | undefined {
75+
const variantLabelLower = variantLabel.toLowerCase();
76+
77+
return variants.find(variant => variant.label?.toLowerCase().includes(variantLabelLower));
78+
}
79+
80+
async function getVariantBuildCommand(
81+
packageJsonPath: string,
82+
variantLabel: string,
83+
testAppPath: string,
84+
): Promise<{ buildCommand: string; assertCommand: string; testLabel: string; matchedVariantLabel?: string }> {
85+
try {
86+
const packageJsonContent = await readFile(packageJsonPath, 'utf-8');
87+
const packageJson: PackageJson = JSON.parse(packageJsonContent);
88+
89+
const allVariants = [
90+
...(packageJson.sentryTest?.variants || []),
91+
...(packageJson.sentryTest?.optionalVariants || []),
92+
];
93+
94+
const matchingVariant = findMatchingVariant(allVariants, variantLabel);
95+
96+
if (matchingVariant) {
97+
return {
98+
buildCommand: matchingVariant['build-command'] || 'pnpm test:build',
99+
assertCommand: matchingVariant['assert-command'] || 'pnpm test:assert',
100+
testLabel: matchingVariant.label || testAppPath,
101+
matchedVariantLabel: matchingVariant.label,
102+
};
103+
}
104+
105+
console.log(`No matching variant found for "${variantLabel}" in ${testAppPath}, using default build`);
106+
} catch {
107+
console.log(`Could not read variants from package.json for ${testAppPath}, using default build`);
108+
}
109+
110+
return {
111+
buildCommand: 'pnpm test:build',
112+
assertCommand: 'pnpm test:assert',
113+
testLabel: testAppPath,
114+
};
115+
}
116+
61117
async function run(): Promise<void> {
62118
// Load environment variables from .env file locally
63119
dotenv.config();
64120

65121
// Allow to run a single app only via `yarn test:run <app-name>`
66122
const appName = process.argv[2] || '';
67123
// Forward any additional flags to the test command
68-
const testFlags = process.argv.slice(3);
124+
const allTestFlags = process.argv.slice(3);
125+
126+
// Check for --variant flag
127+
let variantLabel: string | undefined;
128+
let skipNextFlag = false;
129+
130+
const testFlags = allTestFlags.filter((flag, index) => {
131+
// Skip this flag if it was marked to skip (variant value after --variant)
132+
if (skipNextFlag) {
133+
skipNextFlag = false;
134+
return false;
135+
}
136+
137+
// Handle --variant=<value> format
138+
if (flag.startsWith('--variant=')) {
139+
const value = flag.slice('--variant='.length);
140+
const trimmedValue = value?.trim();
141+
if (trimmedValue) {
142+
variantLabel = trimmedValue;
143+
} else {
144+
console.warn('Warning: --variant= specified but no value provided. Ignoring variant flag.');
145+
}
146+
return false; // Remove this flag from testFlags
147+
}
148+
149+
// Handle --variant <value> format
150+
if (flag === '--variant') {
151+
if (index + 1 < allTestFlags.length) {
152+
const value = allTestFlags[index + 1];
153+
const trimmedValue = value?.trim();
154+
if (trimmedValue) {
155+
variantLabel = trimmedValue;
156+
skipNextFlag = true; // Mark next flag to be skipped
157+
} else {
158+
console.warn('Warning: --variant specified but no value provided. Ignoring variant flag.');
159+
}
160+
} else {
161+
console.warn('Warning: --variant specified but no value provided. Ignoring variant flag.');
162+
}
163+
return false;
164+
}
165+
166+
return true;
167+
});
69168

70169
const dsn = process.env.E2E_TEST_DSN || DEFAULT_DSN;
71170

@@ -107,13 +206,42 @@ async function run(): Promise<void> {
107206

108207
await copyToTemp(originalPath, tmpDirPath);
109208
const cwd = tmpDirPath;
209+
// Resolve variant if needed
210+
const { buildCommand, assertCommand, testLabel, matchedVariantLabel } = variantLabel
211+
? await getVariantBuildCommand(join(tmpDirPath, 'package.json'), variantLabel, testAppPath)
212+
: {
213+
buildCommand: 'pnpm test:build',
214+
assertCommand: 'pnpm test:assert',
215+
testLabel: testAppPath,
216+
};
217+
218+
// Print which variant we're using if found
219+
if (matchedVariantLabel) {
220+
console.log(`\n\nUsing variant: "${matchedVariantLabel}"\n\n`);
221+
}
110222

111-
console.log(`Building ${testAppPath} in ${tmpDirPath}...`);
112-
await asyncExec('volta run pnpm test:build', { env, cwd });
223+
console.log(`Building ${testLabel} in ${tmpDirPath}...`);
224+
await asyncExec(`volta run ${buildCommand}`, { env, cwd });
113225

114-
console.log(`Testing ${testAppPath}...`);
115-
// Pass command and arguments as an array to prevent command injection
116-
const testCommand = ['volta', 'run', 'pnpm', 'test:assert', ...testFlags];
226+
console.log(`Testing ${testLabel}...`);
227+
// Pass command as a string to support shell features (env vars, operators like &&)
228+
// This matches how buildCommand is handled for consistency
229+
// Properly quote test flags to preserve spaces and special characters
230+
const quotedTestFlags = testFlags.map(flag => {
231+
// If flag contains spaces or special shell characters, quote it
232+
if (
233+
flag.includes(' ') ||
234+
flag.includes('"') ||
235+
flag.includes("'") ||
236+
flag.includes('$') ||
237+
flag.includes('`')
238+
) {
239+
// Escape single quotes and wrap in single quotes (safest for shell)
240+
return `'${flag.replace(/'/g, "'\\''")}'`;
241+
}
242+
return flag;
243+
});
244+
const testCommand = `volta run ${assertCommand}${quotedTestFlags.length > 0 ? ` ${quotedTestFlags.join(' ')}` : ''}`;
117245
await asyncExec(testCommand, { env, cwd });
118246

119247
// clean up (although this is tmp, still nice to do)

0 commit comments

Comments
 (0)