From 753fca4989fbb1b2336c9982d4b89a3d4b58a3bb Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 Aug 2025 13:06:55 +0000
Subject: [PATCH 1/5] Initial plan
From acb2e13ba9343221e6b36c338161e2f8022ed88c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 Aug 2025 13:30:39 +0000
Subject: [PATCH 2/5] Changes before error encountered
Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
---
docs/basics.md | 23 +++++++
lib/mocha/ui.js | 13 ++++
.../sandbox/configs/only/codecept.conf.js | 7 ++
.../sandbox/configs/only/edge_case_test.js | 16 +++++
.../configs/only/empty_feature_test.js | 9 +++
test/data/sandbox/configs/only/only_test.js | 29 ++++++++
test/runner/only_test.js | 43 ++++++++++++
test/unit/mocha/ui_test.js | 67 +++++++++++++++++++
8 files changed, 207 insertions(+)
create mode 100644 test/data/sandbox/configs/only/codecept.conf.js
create mode 100644 test/data/sandbox/configs/only/edge_case_test.js
create mode 100644 test/data/sandbox/configs/only/empty_feature_test.js
create mode 100644 test/data/sandbox/configs/only/only_test.js
create mode 100644 test/runner/only_test.js
diff --git a/docs/basics.md b/docs/basics.md
index 39134b4b7..18883a563 100644
--- a/docs/basics.md
+++ b/docs/basics.md
@@ -961,6 +961,29 @@ Like in Mocha you can use `x` and `only` to skip tests or to run a single test.
- `Scenario.only` - executes only the current test
- `xFeature` - skips current suite
- `Feature.skip` - skips the current suite
+- `Feature.only` - executes only the current suite
+
+When using `Feature.only`, only scenarios within that feature will be executed:
+
+```js
+Feature.only('My Important Feature')
+
+Scenario('test something', ({ I }) => {
+ I.amOnPage('https://github.com')
+ I.see('GitHub')
+})
+
+Scenario('test something else', ({ I }) => {
+ I.amOnPage('https://github.com')
+ I.see('GitHub')
+})
+
+Feature('Another Feature') // This will be skipped
+
+Scenario('will not run', ({ I }) => {
+ // This scenario will be skipped
+})
+```
## Todo Test
diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js
index 244ea73f2..196d79ac1 100644
--- a/lib/mocha/ui.js
+++ b/lib/mocha/ui.js
@@ -103,6 +103,19 @@ module.exports = function (suite) {
return new FeatureConfig(suite)
}
+ /**
+ * Exclusive test suite - runs only this feature.
+ * @global
+ * @kind constant
+ * @type {CodeceptJS.IFeature}
+ */
+ context.Feature.only = function (title, opts) {
+ const reString = `^${escapeRe(`${title}:`)}`
+ mocha.grep(new RegExp(reString))
+ process.env.FEATURE_ONLY = true
+ return context.Feature(title, opts)
+ }
+
/**
* Pending test suite.
* @global
diff --git a/test/data/sandbox/configs/only/codecept.conf.js b/test/data/sandbox/configs/only/codecept.conf.js
new file mode 100644
index 000000000..964006f8a
--- /dev/null
+++ b/test/data/sandbox/configs/only/codecept.conf.js
@@ -0,0 +1,7 @@
+exports.config = {
+ tests: './*_test.js',
+ output: './output',
+ bootstrap: null,
+ mocha: {},
+ name: 'only-test',
+}
diff --git a/test/data/sandbox/configs/only/edge_case_test.js b/test/data/sandbox/configs/only/edge_case_test.js
new file mode 100644
index 000000000..37ab2fc2b
--- /dev/null
+++ b/test/data/sandbox/configs/only/edge_case_test.js
@@ -0,0 +1,16 @@
+// Edge case test with special characters and complex titles
+Feature.only('Feature with special chars: @test [brackets] (parens) & symbols')
+
+Scenario('Scenario with special chars: @test [brackets] & symbols', () => {
+ console.log('Special chars scenario executed')
+})
+
+Scenario('Normal scenario', () => {
+ console.log('Normal scenario executed')
+})
+
+Feature('Regular Feature That Should Not Run')
+
+Scenario('Should not run scenario', () => {
+ console.log('This should never execute')
+})
diff --git a/test/data/sandbox/configs/only/empty_feature_test.js b/test/data/sandbox/configs/only/empty_feature_test.js
new file mode 100644
index 000000000..25b85763e
--- /dev/null
+++ b/test/data/sandbox/configs/only/empty_feature_test.js
@@ -0,0 +1,9 @@
+Feature.only('Empty Feature')
+
+// No scenarios in this feature
+
+Feature('Regular Feature')
+
+Scenario('Should not run', () => {
+ console.log('This should not run')
+})
diff --git a/test/data/sandbox/configs/only/only_test.js b/test/data/sandbox/configs/only/only_test.js
new file mode 100644
index 000000000..b5f95fe0e
--- /dev/null
+++ b/test/data/sandbox/configs/only/only_test.js
@@ -0,0 +1,29 @@
+Feature.only('@OnlyFeature')
+
+Scenario('@OnlyScenario1', () => {
+ console.log('Only Scenario 1 was executed')
+})
+
+Scenario('@OnlyScenario2', () => {
+ console.log('Only Scenario 2 was executed')
+})
+
+Scenario('@OnlyScenario3', () => {
+ console.log('Only Scenario 3 was executed')
+})
+
+Feature('@RegularFeature')
+
+Scenario('@RegularScenario1', () => {
+ console.log('Regular Scenario 1 should NOT execute')
+})
+
+Scenario('@RegularScenario2', () => {
+ console.log('Regular Scenario 2 should NOT execute')
+})
+
+Feature('@AnotherRegularFeature')
+
+Scenario('@AnotherRegularScenario', () => {
+ console.log('Another Regular Scenario should NOT execute')
+})
diff --git a/test/runner/only_test.js b/test/runner/only_test.js
new file mode 100644
index 000000000..b71965178
--- /dev/null
+++ b/test/runner/only_test.js
@@ -0,0 +1,43 @@
+const path = require('path')
+const exec = require('child_process').exec
+const assert = require('assert')
+
+const runner = path.join(__dirname, '/../../bin/codecept.js')
+const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/only')
+const codecept_run = `${runner} run --config ${codecept_dir}/codecept.conf.js `
+
+describe('Feature.only', () => {
+ it('should run only scenarios in Feature.only and skip other features', done => {
+ exec(`${codecept_run} only_test.js`, (err, stdout, stderr) => {
+ stdout.should.include('Only Scenario 1 was executed')
+ stdout.should.include('Only Scenario 2 was executed')
+ stdout.should.include('Only Scenario 3 was executed')
+ stdout.should.not.include('Regular Scenario 1 should NOT execute')
+ stdout.should.not.include('Regular Scenario 2 should NOT execute')
+ stdout.should.not.include('Another Regular Scenario should NOT execute')
+
+ // Should show 3 passing tests
+ stdout.should.include('3 passed')
+
+ assert(!err)
+ done()
+ })
+ })
+
+ it('should work when there are multiple features with Feature.only selecting one', done => {
+ exec(`${codecept_run} only_test.js`, (err, stdout, stderr) => {
+ // Should only run the @OnlyFeature scenarios
+ stdout.should.include('@OnlyFeature --')
+ stdout.should.include('✔ @OnlyScenario1')
+ stdout.should.include('✔ @OnlyScenario2')
+ stdout.should.include('✔ @OnlyScenario3')
+
+ // Should not include other features
+ stdout.should.not.include('@RegularFeature')
+ stdout.should.not.include('@AnotherRegularFeature')
+
+ assert(!err)
+ done()
+ })
+ })
+})
diff --git a/test/unit/mocha/ui_test.js b/test/unit/mocha/ui_test.js
index 1de8064f8..34fbf5d92 100644
--- a/test/unit/mocha/ui_test.js
+++ b/test/unit/mocha/ui_test.js
@@ -28,6 +28,11 @@ describe('ui', () => {
constants.forEach(c => {
it(`context should contain ${c}`, () => expect(context[c]).is.ok)
})
+
+ it('context should contain Feature.only', () => {
+ expect(context.Feature.only).is.ok
+ expect(context.Feature.only).to.be.a('function')
+ })
})
describe('Feature', () => {
@@ -129,6 +134,68 @@ describe('ui', () => {
expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info')
})
+ it('Feature can be run exclusively with only', () => {
+ // Create a new mocha instance to test grep behavior
+ const mocha = new Mocha()
+ let grepPattern = null
+
+ // Mock mocha.grep to capture the pattern
+ const originalGrep = mocha.grep
+ mocha.grep = function (pattern) {
+ grepPattern = pattern
+ return this
+ }
+
+ // Reset environment variable
+ delete process.env.FEATURE_ONLY
+
+ // Re-emit pre-require with our mocked mocha instance
+ suite.emit('pre-require', context, {}, mocha)
+
+ suiteConfig = context.Feature.only('exclusive feature', { key: 'value' })
+
+ expect(suiteConfig.suite.title).eq('exclusive feature')
+ expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Feature.only should pass options correctly')
+ expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending')
+ expect(grepPattern).to.be.instanceOf(RegExp)
+ expect(grepPattern.source).eq('^exclusive feature:')
+ expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set')
+
+ // Restore original grep
+ mocha.grep = originalGrep
+ })
+
+ it('Feature.only should work without options', () => {
+ // Create a new mocha instance to test grep behavior
+ const mocha = new Mocha()
+ let grepPattern = null
+
+ // Mock mocha.grep to capture the pattern
+ const originalGrep = mocha.grep
+ mocha.grep = function (pattern) {
+ grepPattern = pattern
+ return this
+ }
+
+ // Reset environment variable
+ delete process.env.FEATURE_ONLY
+
+ // Re-emit pre-require with our mocked mocha instance
+ suite.emit('pre-require', context, {}, mocha)
+
+ suiteConfig = context.Feature.only('exclusive feature without options')
+
+ expect(suiteConfig.suite.title).eq('exclusive feature without options')
+ expect(suiteConfig.suite.opts).to.deep.eq({}, 'Feature.only without options should have empty opts')
+ expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending')
+ expect(grepPattern).to.be.instanceOf(RegExp)
+ expect(grepPattern.source).eq('^exclusive feature without options:')
+ expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set')
+
+ // Restore original grep
+ mocha.grep = originalGrep
+ })
+
it('Feature should correctly pass options to suite context', () => {
suiteConfig = context.Feature('not skipped suite', { key: 'value' })
expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options')
From 0a2650eb8d83dd6f904ce28845cac87fb4e911b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 Aug 2025 14:29:57 +0000
Subject: [PATCH 3/5] Add TypeScript types for Feature.only method
---
typings/index.d.ts | 5 +-
typings/tests/global-variables.types.ts | 146 ++++++++++++++----------
2 files changed, 90 insertions(+), 61 deletions(-)
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 213b222ce..b778aa7fa 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -440,7 +440,7 @@ declare namespace CodeceptJS {
interface IHook {}
interface IScenario {}
interface IFeature {
- (title: string): FeatureConfig
+ (title: string, opts?: { [key: string]: any }): FeatureConfig
}
interface CallbackOrder extends Array {}
interface SupportObject {
@@ -486,6 +486,7 @@ declare namespace CodeceptJS {
todo: IScenario
}
interface Feature extends IFeature {
+ only: IFeature
skip: IFeature
}
interface IData {
@@ -545,7 +546,7 @@ declare const Given: typeof CodeceptJS.addStep
declare const When: typeof CodeceptJS.addStep
declare const Then: typeof CodeceptJS.addStep
-declare const Feature: typeof CodeceptJS.Feature
+declare const Feature: CodeceptJS.Feature
declare const Scenario: CodeceptJS.Scenario
declare const xScenario: CodeceptJS.IScenario
declare const xFeature: CodeceptJS.IFeature
diff --git a/typings/tests/global-variables.types.ts b/typings/tests/global-variables.types.ts
index 2d2a0a512..2910e607f 100644
--- a/typings/tests/global-variables.types.ts
+++ b/typings/tests/global-variables.types.ts
@@ -1,95 +1,123 @@
-import { expectError, expectType } from 'tsd';
+import { expectError, expectType } from 'tsd'
-
-expectError(Feature());
-expectError(Scenario());
-expectError(Before());
-expectError(BeforeSuite());
-expectError(After());
-expectError(AfterSuite());
+expectError(Feature())
+expectError(Scenario())
+expectError(Before())
+expectError(BeforeSuite())
+expectError(After())
+expectError(AfterSuite())
// @ts-ignore
expectType(Feature('feature'))
+// @ts-ignore
+expectType(Feature.only('feature'))
+
+// @ts-ignore
+expectType(Feature.only('feature', {}))
+
+// @ts-ignore
+expectType(Feature.skip('feature'))
+
// @ts-ignore
expectType(Scenario('scenario'))
// @ts-ignore
-expectType(Scenario(
- 'scenario',
- {}, // $ExpectType {}
- () => {} // $ExpectType () => void
-))
+expectType(
+ Scenario(
+ 'scenario',
+ {}, // $ExpectType {}
+ () => {}, // $ExpectType () => void
+ ),
+)
// @ts-ignore
-expectType(Scenario(
- 'scenario',
- () => {} // $ExpectType () => void
-))
+expectType(
+ Scenario(
+ 'scenario',
+ () => {}, // $ExpectType () => void
+ ),
+)
// @ts-ignore
const callback: CodeceptJS.HookCallback = () => {}
// @ts-ignore
-expectType(Scenario(
- 'scenario',
- callback // $ExpectType HookCallback
-))
+expectType(
+ Scenario(
+ 'scenario',
+ callback, // $ExpectType HookCallback
+ ),
+)
// @ts-ignore
-expectType(Scenario('scenario',
- (args) => {
+expectType(
+ Scenario('scenario', args => {
// @ts-ignore
expectType(args)
// @ts-ignore
expectType(args.I) // $ExpectType I
- }
-))
+ }),
+)
// @ts-ignore
-expectType(Scenario(
- 'scenario',
- async () => {} // $ExpectType () => Promise
-))
+expectType(
+ Scenario(
+ 'scenario',
+ async () => {}, // $ExpectType () => Promise
+ ),
+)
// @ts-ignore
-expectType(Before((args) => {
- // @ts-ignore
- expectType(args)
- // @ts-ignore
- expectType(args.I)
-}))
+expectType(
+ Before(args => {
+ // @ts-ignore
+ expectType(args)
+ // @ts-ignore
+ expectType(args.I)
+ }),
+)
// @ts-ignore
-expectType(BeforeSuite((args) => {
- // @ts-ignore
- expectType(args)
- // @ts-ignore
- expectType(args.I)
-}))
+expectType(
+ BeforeSuite(args => {
+ // @ts-ignore
+ expectType(args)
+ // @ts-ignore
+ expectType(args.I)
+ }),
+)
// @ts-ignore
-expectType(After((args) => {
- // @ts-ignore
- expectType(args)
- // @ts-ignore
- expectType(args.I)
-}))
+expectType(
+ After(args => {
+ // @ts-ignore
+ expectType(args)
+ // @ts-ignore
+ expectType(args.I)
+ }),
+)
// @ts-ignore
-expectType(AfterSuite((args) => {
- // @ts-ignore
- expectType(args)
- // @ts-ignore
- expectType(args.I)
-}))
+expectType(
+ AfterSuite(args => {
+ // @ts-ignore
+ expectType(args)
+ // @ts-ignore
+ expectType(args.I)
+ }),
+)
// @ts-ignore
-expectType>(tryTo(() => {
- return true;
-}));
+expectType>(
+ tryTo(() => {
+ return true
+ }),
+)
// @ts-ignore
-expectType>(tryTo(async () => {
- return false;
-}));
+expectType>(
+ tryTo(async () => {
+ return false
+ }),
+)
From abe8b8318c30b5bb0bc8291147de9a0227d87ed4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 Aug 2025 14:31:30 +0000
Subject: [PATCH 4/5] Changes before error encountered
Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
---
.../sandbox/configs/definitions/steps.d.ts | 20 -------------------
1 file changed, 20 deletions(-)
delete mode 100644 test/data/sandbox/configs/definitions/steps.d.ts
diff --git a/test/data/sandbox/configs/definitions/steps.d.ts b/test/data/sandbox/configs/definitions/steps.d.ts
deleted file mode 100644
index 41dc21a1e..000000000
--- a/test/data/sandbox/configs/definitions/steps.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-///
-type steps_file = typeof import('../../support/custom_steps.js')
-type MyPage = typeof import('../../support/my_page.js')
-type SecondPage = typeof import('../../support/second_page.js')
-type CurrentPage = typeof import('./po/custom_steps.js')
-
-declare namespace CodeceptJS {
- interface SupportObject {
- I: I
- current: any
- MyPage: MyPage
- SecondPage: SecondPage
- CurrentPage: CurrentPage
- }
- interface Methods extends FileSystem {}
- interface I extends ReturnType, WithTranslation {}
- namespace Translation {
- interface Actions {}
- }
-}
From 7deb2cf88ae903a76b497ece41d3eab6ac026700 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 21 Aug 2025 15:03:56 +0000
Subject: [PATCH 5/5] Fix TypeScript test expectations for hook return types
Co-authored-by: kobenguyent <7845001+kobenguyent@users.noreply.github.com>
---
typings/tests/global-variables.types.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/typings/tests/global-variables.types.ts b/typings/tests/global-variables.types.ts
index 2910e607f..e887b4932 100644
--- a/typings/tests/global-variables.types.ts
+++ b/typings/tests/global-variables.types.ts
@@ -69,7 +69,7 @@ expectType(
)
// @ts-ignore
-expectType(
+expectType(
Before(args => {
// @ts-ignore
expectType(args)
@@ -79,7 +79,7 @@ expectType(
)
// @ts-ignore
-expectType(
+expectType(
BeforeSuite(args => {
// @ts-ignore
expectType(args)
@@ -89,7 +89,7 @@ expectType(
)
// @ts-ignore
-expectType(
+expectType(
After(args => {
// @ts-ignore
expectType(args)
@@ -99,7 +99,7 @@ expectType(
)
// @ts-ignore
-expectType(
+expectType(
AfterSuite(args => {
// @ts-ignore
expectType(args)