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)