Skip to content

Commit 92b02d4

Browse files
fix(ci-visibility): complete cypress instrumentation coverage
- Resolve default config file when no --config-file is passed, enabling plain-object configs (without defineConfig) in the standard cypress run flow - Skip .ts config files — Cypress transpiles them internally, ESM wrapper can't import .ts directly - Hook both lib/exec/{run,open}.js (Cypress 10-14) and dist/exec/{run,open}.js (Cypress 15+) so the CLI wrapper fires across all supported versions - Respect configFile: false — don't override with a resolved default - Add comments to programmatic test scripts clarifying that instrumentation works via the default config file, not inline configs - Add TypeScript config regression test - Add plain-object default config (no --config-file) regression test - Fix ESM config imports to include .js extension for dd-trace/ci/cypress/plugin Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0b71a93 commit 92b02d4

7 files changed

Lines changed: 144 additions & 6 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
/integration-tests/cypress-plain-object-manual.config.mjs @DataDog/ci-app-libraries
126126
/integration-tests/cypress-return-config.config.js @DataDog/ci-app-libraries
127127
/integration-tests/cypress-return-config.config.mjs @DataDog/ci-app-libraries
128+
/integration-tests/cypress-typescript.config.ts @DataDog/ci-app-libraries
128129
/integration-tests/cypress.config.js @DataDog/ci-app-libraries
129130
/integration-tests/my-nyc.config.js @DataDog/ci-app-libraries
130131
/integration-tests/playwright.config.js @DataDog/ci-app-libraries

integration-tests/cypress-double-run.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict'
22

3+
// Tests that cypress.run() works twice in the same process (resetRunState).
4+
// Instrumentation works via the default cypress.config.js in the project
5+
// (which uses defineConfig), NOT via the inline config below — Cypress
6+
// does not call setupNodeEvents from inline config objects.
37
const cypress = require('cypress')
48

59
const runOptions = {

integration-tests/cypress-double-run.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Tests that cypress.run() works twice in the same process (resetRunState).
2+
// Instrumentation works via the default cypress.config.js in the project
3+
// (which uses defineConfig), NOT via the inline config below — Cypress
4+
// does not call setupNodeEvents from inline config objects.
15
import cypress from 'cypress'
26

37
const runOptions = {

integration-tests/cypress-esm-config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
// Programmatic ESM entry point for the 'esm' module type tests.
2+
// Instrumentation works via the default cypress.config.js in the project
3+
// (which uses defineConfig), NOT via the inline setupNodeEvents below —
4+
// Cypress does not call setupNodeEvents from inline config objects.
15
import cypress from 'cypress'
26

37
async function runCypress () {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { defineConfig } from 'cypress'
2+
3+
export default defineConfig({
4+
defaultCommandTimeout: 1000,
5+
e2e: {
6+
specPattern: process.env.SPEC_PATTERN || 'cypress/e2e/**/*.cy.js',
7+
supportFile: 'cypress/support/e2e.js',
8+
},
9+
video: false,
10+
screenshotOnRunFailure: false,
11+
})

integration-tests/cypress/cypress.spec.js

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ moduleTypes.forEach(({
404404
assert.strictEqual(exitCode, 0, `cypress process should exit successfully\n${testOutput}`)
405405
})
406406

407-
over10It('reports tests when cypress.run is called twice in the same process', async () => {
407+
over10It('reports tests when cypress.run is called twice (multi-run state reset)', async () => {
408408
const receiverPromise = receiver
409409
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => {
410410
const passedTests = payloads
@@ -543,6 +543,101 @@ moduleTypes.forEach(({
543543
}
544544
)
545545

546+
over10It(
547+
'auto-instruments a plain-object default config (no --config-file)',
548+
async () => {
549+
const originalConfig = path.join(cwd, 'cypress.config.js')
550+
const backupConfig = path.join(cwd, 'cypress.config.js.bak')
551+
const plainObjectConfig = path.join(cwd, 'cypress-plain-object-auto.config.js')
552+
553+
// Replace default cypress.config.js with the plain-object config
554+
fs.renameSync(originalConfig, backupConfig)
555+
fs.copyFileSync(plainObjectConfig, originalConfig)
556+
557+
try {
558+
const receiverPromise = receiver
559+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => {
560+
const events = payloads
561+
.flatMap(({ payload }) => payload.events)
562+
.filter(event => event.type === 'test')
563+
const passedTest = events.find(event =>
564+
event.content.resource === 'cypress/e2e/basic-pass.js.basic pass suite can pass'
565+
)
566+
567+
assertObjectContains(passedTest?.content, {
568+
meta: {
569+
[TEST_STATUS]: 'pass',
570+
[TEST_FRAMEWORK]: 'cypress',
571+
},
572+
})
573+
}, 20000)
574+
575+
const envVars = getCiVisAgentlessConfig(receiver.port)
576+
577+
childProcess = exec(
578+
'./node_modules/.bin/cypress run',
579+
{
580+
cwd,
581+
env: {
582+
...envVars,
583+
CYPRESS_BASE_URL: `http://localhost:${webAppPort}`,
584+
SPEC_PATTERN: 'cypress/e2e/basic-pass.js',
585+
},
586+
}
587+
)
588+
589+
const [[exitCode]] = await Promise.all([
590+
once(childProcess, 'exit'),
591+
receiverPromise,
592+
])
593+
594+
assert.strictEqual(exitCode, 0, 'cypress process should exit successfully')
595+
} finally {
596+
fs.renameSync(backupConfig, originalConfig)
597+
}
598+
}
599+
)
600+
601+
over10It('reports tests with a TypeScript config file', async () => {
602+
const receiverPromise = receiver
603+
.gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => {
604+
const events = payloads
605+
.flatMap(({ payload }) => payload.events)
606+
.filter(event => event.type === 'test')
607+
const passedTest = events.find(event =>
608+
event.content.resource === 'cypress/e2e/basic-pass.js.basic pass suite can pass'
609+
)
610+
611+
assertObjectContains(passedTest?.content, {
612+
meta: {
613+
[TEST_STATUS]: 'pass',
614+
[TEST_FRAMEWORK]: 'cypress',
615+
},
616+
})
617+
}, 20000)
618+
619+
const envVars = getCiVisAgentlessConfig(receiver.port)
620+
621+
childProcess = exec(
622+
'./node_modules/.bin/cypress run --config-file cypress-typescript.config.ts',
623+
{
624+
cwd,
625+
env: {
626+
...envVars,
627+
CYPRESS_BASE_URL: `http://localhost:${webAppPort}`,
628+
SPEC_PATTERN: 'cypress/e2e/basic-pass.js',
629+
},
630+
}
631+
)
632+
633+
const [[exitCode]] = await Promise.all([
634+
once(childProcess, 'exit'),
635+
receiverPromise,
636+
])
637+
638+
assert.strictEqual(exitCode, 0, 'cypress process should exit successfully')
639+
})
640+
546641
over10It('does not modify the user support file and cleans up the injected wrapper', async () => {
547642
const supportFilePath = path.join(cwd, 'cypress/support/e2e.js')
548643
const originalSupportContent = fs.readFileSync(supportFilePath, 'utf8')

packages/datadog-instrumentations/src/cypress-config.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,33 @@ function createConfigWrapper (originalConfigFile) {
270270
function wrapCliConfigFileOptions (options) {
271271
const noop = { options, cleanup: () => {} }
272272

273-
if (!options || typeof options.configFile !== 'string') return noop
273+
if (!options) return noop
274274

275275
const projectRoot = typeof options.project === 'string' ? options.project : process.cwd()
276-
const configFilePath = path.isAbsolute(options.configFile)
277-
? options.configFile
278-
: path.resolve(projectRoot, options.configFile)
276+
let configFilePath
277+
278+
if (options.configFile === false) {
279+
// configFile: false means "no config file" — respect Cypress's semantics
280+
return noop
281+
} else if (typeof options.configFile === 'string') {
282+
configFilePath = path.isAbsolute(options.configFile)
283+
? options.configFile
284+
: path.resolve(projectRoot, options.configFile)
285+
} else {
286+
// No explicit --config-file: resolve the default cypress.config.{js,ts,cjs,mjs}
287+
for (const ext of ['.js', '.ts', '.cjs', '.mjs']) {
288+
const candidate = path.join(projectRoot, `cypress.config${ext}`)
289+
if (fs.existsSync(candidate)) {
290+
configFilePath = candidate
291+
break
292+
}
293+
}
294+
}
279295

280-
if (!fs.existsSync(configFilePath)) return noop
296+
// Skip .ts files — Cypress transpiles them internally via its own loader.
297+
// The ESM wrapper can't import .ts directly. The defineConfig shimmer
298+
// handles .ts configs since they're transpiled to CJS by Cypress.
299+
if (!configFilePath || !fs.existsSync(configFilePath) || path.extname(configFilePath) === '.ts') return noop
281300

282301
const wrapperFile = createConfigWrapper(configFilePath)
283302

0 commit comments

Comments
 (0)