Skip to content

Commit b60b68f

Browse files
misc: warn when a --spec pattern matches no spec files (#34023)
* fix(run): warn when a --spec pattern matches no spec files When a user provides multiple spec patterns via `--spec` and one or more patterns don't match any files, Cypress now emits a warning for each unmatched pattern instead of silently skipping them. Fixes #22645 * fix(run): clarify SPEC_FILE_NOT_FOUND warning message Distinguish the per-pattern warning from the NO_SPECS_FOUND error by making it explicit that the pattern was ignored and other specs will still run. * chore: add changelog entry for --spec unmatched pattern warning * chore: move changelog entry into existing Misc section * chore: add SPEC_FILE_NOT_FOUND to schema.graphql and visual snapshot - Add SPEC_FILE_NOT_FOUND to ErrorTypeEnum in schema.graphql (required whenever a new key is added to AllCypressErrors in errors.ts per AGENTS.md) - Add SPEC_FILE_NOT_FOUND.ansi visual snapshot for the new warning type * fix(getUnmatchedPatterns): guard against undefined spec relative paths Integration tests mock ctx.project._specs with plain strings rather than SpecWithRelativeRoot objects, so s.relative is undefined. Filter those out before passing to minimatch to prevent "Cannot read properties of undefined (reading 'split')" crashes in tests that don't exercise the --spec warning path. * fix(getUnmatchedPatterns): handle Windows path separators and absolute paths relativeSpecPattern uses a hardcoded '/' separator so it cannot strip an absolute Windows path (e.g. C:\project\cypress\e2e\foo.cy.ts stays absolute). Likewise, path.relative returns backslash-separated paths on Windows while minimatch treats backslashes as escape characters, not path separators. Two fixes: - run.ts: fall back to path.relative() for any pattern still absolute after relativeSpecPattern, preventing false SPEC_FILE_NOT_FOUND warnings on Windows - getUnmatchedPatterns: normalize all paths to forward slashes before minimatch so the comparison is correct on all platforms * feat(run): consolidate multiple unmatched --spec warnings into one When multiple --spec patterns match nothing, emit a single SPEC_FILE_NOT_FOUND warning listing all unmatched patterns together rather than one warning per pattern. - errors.ts: SPEC_FILE_NOT_FOUND now accepts string | string[] and uses fmt.listItems so all unmatched paths appear under one header - run.ts: replace per-pattern warning loop with a single warning call - Add multiplePatterns snapshot variant to visualSnapshotErrors.spec.ts --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e0fe6e0 commit b60b68f

9 files changed

Lines changed: 115 additions & 0 deletions

File tree

cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848
- When Cypress cannot connect to a Chromium-based browser such as Chrome or Edge over the Chrome DevTools Protocol, the resulting error now suggests checking whether remote debugging has been disabled by an enterprise or group policy. Because Cypress relies on remote debugging to control the browser, the `RemoteDebuggingAllowed` policy being disabled prevents Cypress from connecting, and the error now points to `chrome://policy` or `edge://policy` to investigate. Addresses [#32526](https://github.com/cypress-io/cypress/issues/32526).
4949
- Improved TypeScript types for `cy.get('@alias')` and `cy.wait('@alias')` so that `@`-prefixed strings correctly resolve to `Chainable<S>` instead of `Chainable<JQuery<HTMLElement>>`. Addresses [#8762](https://github.com/cypress-io/cypress/issues/8762).
50+
- When running `cypress run --spec` with multiple patterns and one or more patterns do not match any spec files, Cypress now emits a warning identifying each unmatched pattern instead of silently skipping it. Addresses [#22645](https://github.com/cypress-io/cypress/issues/22645).
5051

5152
**Dependency Updates:**
5253

packages/data-context/schemas/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ enum ErrorTypeEnum {
13001300
RUN_GROUPING_FEATURE_NOT_AVAILABLE_IN_PLAN
13011301
SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR
13021302
SETUP_NODE_EVENTS_IS_NOT_FUNCTION
1303+
SPEC_FILE_NOT_FOUND
13031304
SUPPORT_FILE_NOT_FOUND
13041305
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED
13051306
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET

packages/data-context/src/sources/ProjectDataSource.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,21 @@ export class ProjectDataSource {
537537
return this.ctx.project.specs.find((x) => x.absolute === absolute)
538538
}
539539

540+
getUnmatchedPatterns (patterns: string[], specs: SpecWithRelativeRoot[]): string[] {
541+
const MINIMATCH_OPTIONS = { dot: true, matchBase: true }
542+
// Normalize to forward slashes: path.relative may return backslashes on Windows and
543+
// minimatch treats backslashes as escape characters rather than path separators.
544+
const normalize = (p: string) => p.split(path.sep).join('/')
545+
const specRelativePaths = specs
546+
.map((s) => s.relative)
547+
.filter((p): p is string => typeof p === 'string')
548+
.map(normalize)
549+
550+
return patterns.filter((pattern) => {
551+
return !specRelativePaths.some((specPath) => minimatch(specPath, normalize(pattern), MINIMATCH_OPTIONS))
552+
})
553+
}
554+
540555
async getProjectPreferences (projectTitle: string) {
541556
const preferences = await this.api.getProjectPreferencesFromCache()
542557

packages/data-context/test/unit/sources/ProjectDataSource.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,4 +898,58 @@ describe('ProjectDataSource', () => {
898898
})
899899
})
900900
})
901+
902+
describe('#getUnmatchedPatterns', () => {
903+
const makeSpec = (relative: string) => ({ relative } as import('@packages/types').SpecWithRelativeRoot)
904+
905+
it('returns empty array when all patterns match found specs', () => {
906+
const specs = [
907+
makeSpec('cypress/e2e/foo.cy.ts'),
908+
makeSpec('cypress/e2e/bar.cy.ts'),
909+
]
910+
911+
const unmatched = ctx.project.getUnmatchedPatterns(
912+
['cypress/e2e/foo.cy.ts', 'cypress/e2e/bar.cy.ts'],
913+
specs,
914+
)
915+
916+
expect(unmatched).toEqual([])
917+
})
918+
919+
it('returns patterns that did not match any spec', () => {
920+
const specs = [
921+
makeSpec('cypress/e2e/foo.cy.ts'),
922+
]
923+
924+
const unmatched = ctx.project.getUnmatchedPatterns(
925+
['cypress/e2e/foo.cy.ts', 'cypress/e2e/nonexistent.cy.ts'],
926+
specs,
927+
)
928+
929+
expect(unmatched).toEqual(['cypress/e2e/nonexistent.cy.ts'])
930+
})
931+
932+
it('returns all patterns when no specs are found', () => {
933+
const unmatched = ctx.project.getUnmatchedPatterns(
934+
['cypress/e2e/foo.cy.ts', 'cypress/e2e/bar.cy.ts'],
935+
[],
936+
)
937+
938+
expect(unmatched).toEqual(['cypress/e2e/foo.cy.ts', 'cypress/e2e/bar.cy.ts'])
939+
})
940+
941+
it('correctly matches glob patterns against found specs', () => {
942+
const specs = [
943+
makeSpec('cypress/e2e/foo.cy.ts'),
944+
makeSpec('cypress/e2e/bar.cy.ts'),
945+
]
946+
947+
const unmatched = ctx.project.getUnmatchedPatterns(
948+
['cypress/e2e/**/*.cy.ts', 'cypress/e2e/nonexistent.cy.ts'],
949+
specs,
950+
)
951+
952+
expect(unmatched).toEqual(['cypress/e2e/nonexistent.cy.ts'])
953+
})
954+
})
901955
})

packages/errors/src/errors.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,21 @@ export const AllCypressErrors = {
758758
759759
${fmt.listItems(globPaths, { color: 'blue', prefix: ' > ' })}`
760760
},
761+
SPEC_FILE_NOT_FOUND: (folderPath: string, patterns: string | string[]) => {
762+
const patternList = Array.isArray(patterns) ? patterns : [patterns]
763+
const globPaths = patternList.map((pattern) => {
764+
const [resolvedBasePath, resolvedPattern] = parseResolvedPattern(folderPath, pattern)
765+
766+
return path.join(resolvedBasePath!, theme.yellow(resolvedPattern!))
767+
})
768+
769+
return errTemplate`\
770+
The following ${fmt.flag('--spec')} pattern did not match any spec files and will be ignored:
771+
772+
${fmt.listItems(globPaths, { color: 'blue', prefix: ' > ' })}
773+
774+
Other spec files that matched will still run.`
775+
},
761776
RENDERER_CRASHED: (browserName: string) => {
762777
return errTemplate`\
763778
We detected that the ${fmt.highlight(browserName)} Renderer process just crashed.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
The following --spec pattern did not match any spec files and will be ignored:
2+

3+
 > /path/to/project/root/cypress/e2e/nonexistent.cy.ts
4+
 > /path/to/project/root/cypress/e2e/also-not-found.cy.ts
5+

6+
Other spec files that matched will still run.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
The following --spec pattern did not match any spec files and will be ignored:
2+

3+
 > /path/to/project/root/cypress/e2e/nonexistent.cy.ts
4+

5+
Other spec files that matched will still run.

packages/errors/test/visualSnapshotErrors.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,12 @@ describe('visual error templates', () => {
628628
noPattern: ['/path/to/project/root'],
629629
}
630630
},
631+
SPEC_FILE_NOT_FOUND: () => {
632+
return {
633+
default: ['/path/to/project/root', 'cypress/e2e/nonexistent.cy.ts'],
634+
multiplePatterns: ['/path/to/project/root', ['cypress/e2e/nonexistent.cy.ts', 'cypress/e2e/also-not-found.cy.ts']],
635+
}
636+
},
631637
RENDERER_CRASHED: () => {
632638
return {
633639
default: ['Electron'],

packages/server/lib/modes/run.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,18 @@ async function ready (options: ReadyOptions) {
11641164
errors.throwErr('NO_SPECS_FOUND', projectRoot, String(specPattern))
11651165
}
11661166

1167+
if (specPatternFromCli) {
1168+
const rawPatterns = Array.isArray(specPattern) ? specPattern : [specPattern as string]
1169+
// relativeSpecPattern uses a forward-slash concat and may not strip Windows absolute paths;
1170+
// fall back to path.relative for any pattern that remains absolute.
1171+
const relativePatterns = rawPatterns.map((p) => path.isAbsolute(p) ? path.relative(projectRoot, p) : p)
1172+
const unmatchedPatterns = project.ctx.project.getUnmatchedPatterns(relativePatterns, specs)
1173+
1174+
if (unmatchedPatterns.length > 0) {
1175+
errors.warning('SPEC_FILE_NOT_FOUND', projectRoot, unmatchedPatterns)
1176+
}
1177+
}
1178+
11671179
if (browser.unsupportedVersion && browser.warning) {
11681180
errors.throwErr('UNSUPPORTED_BROWSER_VERSION', browser.warning)
11691181
}

0 commit comments

Comments
 (0)