|
1 | 1 | /* eslint-disable no-console */ |
2 | 2 | import { spawn } from 'child_process'; |
3 | 3 | import * as dotenv from 'dotenv'; |
4 | | -import { mkdtemp, rm } from 'fs/promises'; |
| 4 | +import { mkdtemp, readFile, rm } from 'fs/promises'; |
5 | 5 | import { sync as globSync } from 'glob'; |
6 | 6 | import { tmpdir } from 'os'; |
7 | 7 | import { join, resolve } from 'path'; |
8 | 8 | import { copyToTemp } from './lib/copyToTemp'; |
9 | 9 | import { registrySetup } from './registrySetup'; |
10 | 10 |
|
| 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 | + |
11 | 24 | const DEFAULT_DSN = 'https://username@domain/123'; |
12 | 25 | const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; |
13 | 26 | const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; |
@@ -58,14 +71,100 @@ function asyncExec( |
58 | 71 | }); |
59 | 72 | } |
60 | 73 |
|
| 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 | + |
61 | 117 | async function run(): Promise<void> { |
62 | 118 | // Load environment variables from .env file locally |
63 | 119 | dotenv.config(); |
64 | 120 |
|
65 | 121 | // Allow to run a single app only via `yarn test:run <app-name>` |
66 | 122 | const appName = process.argv[2] || ''; |
67 | 123 | // 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 | + }); |
69 | 168 |
|
70 | 169 | const dsn = process.env.E2E_TEST_DSN || DEFAULT_DSN; |
71 | 170 |
|
@@ -107,13 +206,42 @@ async function run(): Promise<void> { |
107 | 206 |
|
108 | 207 | await copyToTemp(originalPath, tmpDirPath); |
109 | 208 | 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 | + } |
110 | 222 |
|
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 }); |
113 | 225 |
|
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(' ')}` : ''}`; |
117 | 245 | await asyncExec(testCommand, { env, cwd }); |
118 | 246 |
|
119 | 247 | // clean up (although this is tmp, still nice to do) |
|
0 commit comments