From a742eee84ff6172438896ecafdd84eb2d878db44 Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Mon, 9 Nov 2026 17:42:50 +0100 Subject: [PATCH 1/2] feat: support current datatable for before and after hooks --- lib/data/context.js | 16 ++++ lib/mocha/hooks.js | 5 ++ .../sandbox/codecept.dataDrivenHooks.json | 14 ++++ .../dataDrivenHooks_test.dataDrivenHooks.js | 25 ++++++ test/unit/data/dataDrivenHooks_test.js | 83 +++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 test/data/sandbox/codecept.dataDrivenHooks.json create mode 100644 test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js create mode 100644 test/unit/data/dataDrivenHooks_test.js diff --git a/lib/data/context.js b/lib/data/context.js index 50936b8e0..1e2617619 100644 --- a/lib/data/context.js +++ b/lib/data/context.js @@ -26,6 +26,22 @@ export default function (context) { maskSecretInTitle(scenarios) return new DataScenarioConfig(scenarios) }, + Before(fn) { + data.forEach(dataRow => { + if (!dataRow.skip) { + const wrappedFn = (args) => fn({ ...args, current: dataRow.data }) + context.Before(wrappedFn) + } + }) + }, + After(fn) { + data.forEach(dataRow => { + if (!dataRow.skip) { + const wrappedFn = (args) => fn({ ...args, current: dataRow.data }) + context.After(wrappedFn) + } + }) + }, only: { Scenario(title, opts, fn) { const scenarios = [] diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index ded7f7b42..edfabc71d 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -110,6 +110,11 @@ class HookConfig { this.hook.retry(n) return this } + + inject(obj) { + this.hook.inject = obj + return this + } } export { diff --git a/test/data/sandbox/codecept.dataDrivenHooks.json b/test/data/sandbox/codecept.dataDrivenHooks.json new file mode 100644 index 000000000..48ca630cb --- /dev/null +++ b/test/data/sandbox/codecept.dataDrivenHooks.json @@ -0,0 +1,14 @@ +{ + "tests": "./*_test.dataDrivenHooks.js", + "timeout": 10000, + "output": "./output", + "helpers": { + "FakeDriver": { + "require": "../helper" + } + }, + "include": {}, + "bootstrap": false, + "mocha": {}, + "name": "sandbox" +} diff --git a/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js b/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js new file mode 100644 index 000000000..0ec4d433e --- /dev/null +++ b/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js @@ -0,0 +1,25 @@ +Feature('Data-driven Before/After hooks'); + +import { dataTable as DataTable } from 'codeceptjs'; + +const hookCalls = []; + +const credentials = new DataTable(['username', 'password']); +credentials.add(['alice', 'pass123']); +credentials.add(['bob', 'pass456']); + +Data(credentials).Before(({ current }) => { + hookCalls.push(`Before: ${current.username}`); +}); + +Data(credentials).After(({ current }) => { + hookCalls.push(`After: ${current.username}`); +}); + +Scenario('test 1', () => { + // Just ensure test runs +}); + +Scenario('test 2', () => { + // Just ensure test runs +}); diff --git a/test/unit/data/dataDrivenHooks_test.js b/test/unit/data/dataDrivenHooks_test.js new file mode 100644 index 000000000..6d7a71e6d --- /dev/null +++ b/test/unit/data/dataDrivenHooks_test.js @@ -0,0 +1,83 @@ +import { expect } from 'chai' +import DataTable from '../../../lib/data/table.js' + +describe('Data-driven Before/After hooks - data structure', () => { + it('should create DataTable with correct structure', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + accounts.add(['admin', '654321']) + + expect(accounts.rows).to.have.lengthOf(2) + expect(accounts.array).to.deep.equal(['login', 'password']) + }) + + it('should have rows property with data and skip', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + + accounts.rows.forEach(row => { + expect(row).to.have.property('data') + expect(row).to.have.property('skip') + expect(row.data).to.include.keys(['login', 'password']) + }) + }) + + it('first row should have correct data', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + accounts.add(['admin', '654321']) + + const firstRow = accounts.rows[0] + expect(firstRow.data.login).to.equal('davert') + expect(firstRow.data.password).to.equal('123456') + }) + + it('second row should have correct data', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + accounts.add(['admin', '654321']) + + const secondRow = accounts.rows[1] + expect(secondRow.data.login).to.equal('admin') + expect(secondRow.data.password).to.equal('654321') + }) + + it('should support filtering skipped rows', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + accounts.add(['admin', '654321']) + + const activeRows = accounts.rows.filter(row => !row.skip) + expect(activeRows).to.have.lengthOf(2) + }) + + it('should extract current from row data', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + + const row = accounts.rows[0] + const current = row.data + + expect(current.login).to.equal('davert') + expect(current.password).to.equal('123456') + }) + + it('should support iterating all rows', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + accounts.add(['admin', '654321']) + + const currentValues = accounts.rows.map(row => row.data) + expect(currentValues).to.have.lengthOf(2) + expect(currentValues[0].login).to.equal('davert') + expect(currentValues[1].login).to.equal('admin') + }) + + it('should detect when row is skipped', () => { + const accounts = new DataTable(['login', 'password']) + accounts.add(['davert', '123456']) + + const firstRow = accounts.rows[0] + expect(firstRow.skip).to.be.false + }) +}) From ead1b7ab07e144c76c5960ccb991f3fdea4f7c6a Mon Sep 17 00:00:00 2001 From: kobenguyent Date: Tue, 10 Nov 2026 16:31:11 +0100 Subject: [PATCH 2/2] address PR --- lib/data/context.js | 12 +- lib/mocha/hooks.js | 5 - .../dataDrivenHooks_test.dataDrivenHooks.js | 22 ++- test/unit/data/dataDrivenHooks_test.js | 136 ++++++++++++++++++ 4 files changed, 164 insertions(+), 11 deletions(-) diff --git a/lib/data/context.js b/lib/data/context.js index 1e2617619..cf288c8cf 100644 --- a/lib/data/context.js +++ b/lib/data/context.js @@ -29,7 +29,11 @@ export default function (context) { Before(fn) { data.forEach(dataRow => { if (!dataRow.skip) { - const wrappedFn = (args) => fn({ ...args, current: dataRow.data }) + const wrappedFn = (args) => { + if (args && args.test && args.test.inject && args.test.inject.current === dataRow.data) { + return fn({ ...args, current: dataRow.data }) + } + } context.Before(wrappedFn) } }) @@ -37,7 +41,11 @@ export default function (context) { After(fn) { data.forEach(dataRow => { if (!dataRow.skip) { - const wrappedFn = (args) => fn({ ...args, current: dataRow.data }) + const wrappedFn = (args) => { + if (args && args.test && args.test.inject && args.test.inject.current === dataRow.data) { + return fn({ ...args, current: dataRow.data }) + } + } context.After(wrappedFn) } }) diff --git a/lib/mocha/hooks.js b/lib/mocha/hooks.js index edfabc71d..ded7f7b42 100644 --- a/lib/mocha/hooks.js +++ b/lib/mocha/hooks.js @@ -110,11 +110,6 @@ class HookConfig { this.hook.retry(n) return this } - - inject(obj) { - this.hook.inject = obj - return this - } } export { diff --git a/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js b/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js index 0ec4d433e..bb91d110b 100644 --- a/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js +++ b/test/data/sandbox/dataDrivenHooks_test.dataDrivenHooks.js @@ -16,10 +16,24 @@ Data(credentials).After(({ current }) => { hookCalls.push(`After: ${current.username}`); }); -Scenario('test 1', () => { - // Just ensure test runs +Data(credentials).Scenario('test with data', ({ I, current }) => { + if (!current) { + throw new Error('Test should receive current data from Data().Scenario()'); + } + if (!current.username || !current.password) { + throw new Error(`Test data incomplete: ${JSON.stringify(current)}`); + } }); -Scenario('test 2', () => { - // Just ensure test runs +Scenario('test without data', () => { + console.log('hookCalls:', hookCalls); + if (hookCalls.length > 0) { + // Verify that hooks ran correctly for data-driven scenarios + const expectedCallsPerScenario = 2; // Before + After for each data row + const dataRowCount = 2; + const expectedTotalCalls = expectedCallsPerScenario * dataRowCount; + if (hookCalls.length !== expectedTotalCalls) { + throw new Error(`Expected ${expectedTotalCalls} hook calls, but got ${hookCalls.length}: ${hookCalls.join(', ')}`); + } + } }); diff --git a/test/unit/data/dataDrivenHooks_test.js b/test/unit/data/dataDrivenHooks_test.js index 6d7a71e6d..0eb836e75 100644 --- a/test/unit/data/dataDrivenHooks_test.js +++ b/test/unit/data/dataDrivenHooks_test.js @@ -1,5 +1,6 @@ import { expect } from 'chai' import DataTable from '../../../lib/data/table.js' +import addDataContext from '../../../lib/data/context.js' describe('Data-driven Before/After hooks - data structure', () => { it('should create DataTable with correct structure', () => { @@ -81,3 +82,138 @@ describe('Data-driven Before/After hooks - data structure', () => { expect(firstRow.skip).to.be.false }) }) + +describe('Data-driven Before/After hooks - hook methods', () => { + it('should have Before and After methods on Data object', () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + + const mockContext = {} + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + expect(data).to.have.property('Before') + expect(data).to.have.property('After') + expect(data.Before).to.be.a('function') + expect(data.After).to.be.a('function') + }) + + it('should register one Before hook per non-skipped data row', () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + credentials.add(['bob', 'pass456']) + + let hookCount = 0 + const mockContext = { + Before: (fn) => { + hookCount++ + }, + } + + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + data.Before(() => {}) + + expect(hookCount).to.equal(2) + }) + + it('should skip rows marked as skipped when registering Before hooks', () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + + credentials.rows[0].skip = true + + let hookCount = 0 + const mockContext = { + Before: (fn) => { + hookCount++ + }, + } + + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + data.Before(() => {}) + + expect(hookCount).to.equal(0) + }) + + it('should pass current data to Before hook when test matches data row', async () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + + const hookCalls = [] + const mockContext = { + Before: (fn) => { + hookCalls.push(fn) + }, + } + + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + data.Before(({ current }) => { + return current + }) + + const aliceData = credentials.rows[0].data + const testWithMatchingData = { + inject: { + current: aliceData, + }, + } + + const result = await hookCalls[0]({ test: testWithMatchingData }) + expect(result).to.have.property('username', 'alice') + expect(result).to.have.property('password', 'pass123') + }) + + it('should not execute Before hook when test data does not match data row', async () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + + const hookCalls = [] + const mockContext = { + Before: (fn) => { + hookCalls.push(fn) + }, + } + + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + data.Before(({ current }) => { + return current + }) + + const testWithDifferentData = { + inject: { + current: { username: 'bob', password: 'pass456' }, + }, + } + + const result = await hookCalls[0]({ test: testWithDifferentData }) + expect(result).to.be.undefined + }) + + it('should register one After hook per non-skipped data row', () => { + const credentials = new DataTable(['username', 'password']) + credentials.add(['alice', 'pass123']) + credentials.add(['bob', 'pass456']) + + let hookCount = 0 + const mockContext = { + After: (fn) => { + hookCount++ + }, + } + + addDataContext(mockContext) + + const data = mockContext.Data(credentials) + data.After(() => {}) + + expect(hookCount).to.equal(2) + }) +})