Skip to content

Commit ce16e92

Browse files
Copilotkobenguyent
andauthored
[FR] - Support feature.only like Scenario.only (#5087)
* Initial plan * Changes before error encountered Co-authored-by: kobenguyent <[email protected]> * Add TypeScript types for Feature.only method * Changes before error encountered Co-authored-by: kobenguyent <[email protected]> * Fix TypeScript test expectations for hook return types Co-authored-by: kobenguyent <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: kobenguyent <[email protected]>
1 parent f295302 commit ce16e92

File tree

11 files changed

+297
-81
lines changed

11 files changed

+297
-81
lines changed

docs/basics.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,29 @@ Like in Mocha you can use `x` and `only` to skip tests or to run a single test.
961961
- `Scenario.only` - executes only the current test
962962
- `xFeature` - skips current suite <Badge text="Since 2.6.6" type="warning"/>
963963
- `Feature.skip` - skips the current suite <Badge text="Since 2.6.6" type="warning"/>
964+
- `Feature.only` - executes only the current suite <Badge text="Since 3.7.5" type="warning"/>
965+
966+
When using `Feature.only`, only scenarios within that feature will be executed:
967+
968+
```js
969+
Feature.only('My Important Feature')
970+
971+
Scenario('test something', ({ I }) => {
972+
I.amOnPage('https://github.com')
973+
I.see('GitHub')
974+
})
975+
976+
Scenario('test something else', ({ I }) => {
977+
I.amOnPage('https://github.com')
978+
I.see('GitHub')
979+
})
980+
981+
Feature('Another Feature') // This will be skipped
982+
983+
Scenario('will not run', ({ I }) => {
984+
// This scenario will be skipped
985+
})
986+
```
964987
965988
## Todo Test
966989

lib/mocha/ui.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,19 @@ module.exports = function (suite) {
103103
return new FeatureConfig(suite)
104104
}
105105

106+
/**
107+
* Exclusive test suite - runs only this feature.
108+
* @global
109+
* @kind constant
110+
* @type {CodeceptJS.IFeature}
111+
*/
112+
context.Feature.only = function (title, opts) {
113+
const reString = `^${escapeRe(`${title}:`)}`
114+
mocha.grep(new RegExp(reString))
115+
process.env.FEATURE_ONLY = true
116+
return context.Feature(title, opts)
117+
}
118+
106119
/**
107120
* Pending test suite.
108121
* @global

test/data/sandbox/configs/definitions/steps.d.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
exports.config = {
2+
tests: './*_test.js',
3+
output: './output',
4+
bootstrap: null,
5+
mocha: {},
6+
name: 'only-test',
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Edge case test with special characters and complex titles
2+
Feature.only('Feature with special chars: @test [brackets] (parens) & symbols')
3+
4+
Scenario('Scenario with special chars: @test [brackets] & symbols', () => {
5+
console.log('Special chars scenario executed')
6+
})
7+
8+
Scenario('Normal scenario', () => {
9+
console.log('Normal scenario executed')
10+
})
11+
12+
Feature('Regular Feature That Should Not Run')
13+
14+
Scenario('Should not run scenario', () => {
15+
console.log('This should never execute')
16+
})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Feature.only('Empty Feature')
2+
3+
// No scenarios in this feature
4+
5+
Feature('Regular Feature')
6+
7+
Scenario('Should not run', () => {
8+
console.log('This should not run')
9+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Feature.only('@OnlyFeature')
2+
3+
Scenario('@OnlyScenario1', () => {
4+
console.log('Only Scenario 1 was executed')
5+
})
6+
7+
Scenario('@OnlyScenario2', () => {
8+
console.log('Only Scenario 2 was executed')
9+
})
10+
11+
Scenario('@OnlyScenario3', () => {
12+
console.log('Only Scenario 3 was executed')
13+
})
14+
15+
Feature('@RegularFeature')
16+
17+
Scenario('@RegularScenario1', () => {
18+
console.log('Regular Scenario 1 should NOT execute')
19+
})
20+
21+
Scenario('@RegularScenario2', () => {
22+
console.log('Regular Scenario 2 should NOT execute')
23+
})
24+
25+
Feature('@AnotherRegularFeature')
26+
27+
Scenario('@AnotherRegularScenario', () => {
28+
console.log('Another Regular Scenario should NOT execute')
29+
})

test/runner/only_test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const path = require('path')
2+
const exec = require('child_process').exec
3+
const assert = require('assert')
4+
5+
const runner = path.join(__dirname, '/../../bin/codecept.js')
6+
const codecept_dir = path.join(__dirname, '/../data/sandbox/configs/only')
7+
const codecept_run = `${runner} run --config ${codecept_dir}/codecept.conf.js `
8+
9+
describe('Feature.only', () => {
10+
it('should run only scenarios in Feature.only and skip other features', done => {
11+
exec(`${codecept_run} only_test.js`, (err, stdout, stderr) => {
12+
stdout.should.include('Only Scenario 1 was executed')
13+
stdout.should.include('Only Scenario 2 was executed')
14+
stdout.should.include('Only Scenario 3 was executed')
15+
stdout.should.not.include('Regular Scenario 1 should NOT execute')
16+
stdout.should.not.include('Regular Scenario 2 should NOT execute')
17+
stdout.should.not.include('Another Regular Scenario should NOT execute')
18+
19+
// Should show 3 passing tests
20+
stdout.should.include('3 passed')
21+
22+
assert(!err)
23+
done()
24+
})
25+
})
26+
27+
it('should work when there are multiple features with Feature.only selecting one', done => {
28+
exec(`${codecept_run} only_test.js`, (err, stdout, stderr) => {
29+
// Should only run the @OnlyFeature scenarios
30+
stdout.should.include('@OnlyFeature --')
31+
stdout.should.include('✔ @OnlyScenario1')
32+
stdout.should.include('✔ @OnlyScenario2')
33+
stdout.should.include('✔ @OnlyScenario3')
34+
35+
// Should not include other features
36+
stdout.should.not.include('@RegularFeature')
37+
stdout.should.not.include('@AnotherRegularFeature')
38+
39+
assert(!err)
40+
done()
41+
})
42+
})
43+
})

test/unit/mocha/ui_test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ describe('ui', () => {
2828
constants.forEach(c => {
2929
it(`context should contain ${c}`, () => expect(context[c]).is.ok)
3030
})
31+
32+
it('context should contain Feature.only', () => {
33+
expect(context.Feature.only).is.ok
34+
expect(context.Feature.only).to.be.a('function')
35+
})
3136
})
3237

3338
describe('Feature', () => {
@@ -129,6 +134,68 @@ describe('ui', () => {
129134
expect(suiteConfig.suite.opts).to.deep.eq({}, 'Features should have no skip info')
130135
})
131136

137+
it('Feature can be run exclusively with only', () => {
138+
// Create a new mocha instance to test grep behavior
139+
const mocha = new Mocha()
140+
let grepPattern = null
141+
142+
// Mock mocha.grep to capture the pattern
143+
const originalGrep = mocha.grep
144+
mocha.grep = function (pattern) {
145+
grepPattern = pattern
146+
return this
147+
}
148+
149+
// Reset environment variable
150+
delete process.env.FEATURE_ONLY
151+
152+
// Re-emit pre-require with our mocked mocha instance
153+
suite.emit('pre-require', context, {}, mocha)
154+
155+
suiteConfig = context.Feature.only('exclusive feature', { key: 'value' })
156+
157+
expect(suiteConfig.suite.title).eq('exclusive feature')
158+
expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Feature.only should pass options correctly')
159+
expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending')
160+
expect(grepPattern).to.be.instanceOf(RegExp)
161+
expect(grepPattern.source).eq('^exclusive feature:')
162+
expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set')
163+
164+
// Restore original grep
165+
mocha.grep = originalGrep
166+
})
167+
168+
it('Feature.only should work without options', () => {
169+
// Create a new mocha instance to test grep behavior
170+
const mocha = new Mocha()
171+
let grepPattern = null
172+
173+
// Mock mocha.grep to capture the pattern
174+
const originalGrep = mocha.grep
175+
mocha.grep = function (pattern) {
176+
grepPattern = pattern
177+
return this
178+
}
179+
180+
// Reset environment variable
181+
delete process.env.FEATURE_ONLY
182+
183+
// Re-emit pre-require with our mocked mocha instance
184+
suite.emit('pre-require', context, {}, mocha)
185+
186+
suiteConfig = context.Feature.only('exclusive feature without options')
187+
188+
expect(suiteConfig.suite.title).eq('exclusive feature without options')
189+
expect(suiteConfig.suite.opts).to.deep.eq({}, 'Feature.only without options should have empty opts')
190+
expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending')
191+
expect(grepPattern).to.be.instanceOf(RegExp)
192+
expect(grepPattern.source).eq('^exclusive feature without options:')
193+
expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set')
194+
195+
// Restore original grep
196+
mocha.grep = originalGrep
197+
})
198+
132199
it('Feature should correctly pass options to suite context', () => {
133200
suiteConfig = context.Feature('not skipped suite', { key: 'value' })
134201
expect(suiteConfig.suite.opts).to.deep.eq({ key: 'value' }, 'Features should have passed options')

typings/index.d.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ declare namespace CodeceptJS {
440440
interface IHook {}
441441
interface IScenario {}
442442
interface IFeature {
443-
(title: string): FeatureConfig
443+
(title: string, opts?: { [key: string]: any }): FeatureConfig
444444
}
445445
interface CallbackOrder extends Array<any> {}
446446
interface SupportObject {
@@ -486,6 +486,7 @@ declare namespace CodeceptJS {
486486
todo: IScenario
487487
}
488488
interface Feature extends IFeature {
489+
only: IFeature
489490
skip: IFeature
490491
}
491492
interface IData {
@@ -545,7 +546,7 @@ declare const Given: typeof CodeceptJS.addStep
545546
declare const When: typeof CodeceptJS.addStep
546547
declare const Then: typeof CodeceptJS.addStep
547548

548-
declare const Feature: typeof CodeceptJS.Feature
549+
declare const Feature: CodeceptJS.Feature
549550
declare const Scenario: CodeceptJS.Scenario
550551
declare const xScenario: CodeceptJS.IScenario
551552
declare const xFeature: CodeceptJS.IFeature

0 commit comments

Comments
 (0)