Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/mocha/asyncWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export function setup(suite) {
recorder.startUnlessRunning()
import('./test.js').then(testModule => {
const { enhanceMochaTest } = testModule.default || testModule
event.emit(event.test.before, enhanceMochaTest(suite?.currentTest ?? suite?.ctx?.currentTest))
event.emit(event.test.before, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
recorder.add(() => doneFn())
})
}
Expand All @@ -211,7 +211,7 @@ export function teardown(suite) {
recorder.startUnlessRunning()
import('./test.js').then(testModule => {
const { enhanceMochaTest } = testModule.default || testModule
event.emit(event.test.after, enhanceMochaTest(suite?.currentTest ?? suite?.ctx?.currentTest))
event.emit(event.test.after, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
recorder.add(() => doneFn())
})
}
Expand Down
9 changes: 9 additions & 0 deletions test/acceptance/gherkin/before_hook.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@Playwright @Puppeteer @WebDriverIO @bdd @hookCapture
Feature: Before hooks receive the real scenario

Background:
Given I opened website

@scenarioHook
Scenario: Before hook captures scenario metadata
Then the Before hook should have captured this scenario
61 changes: 61 additions & 0 deletions test/acceptance/gherkin/steps.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,71 @@
const I = actor();
const event = codeceptjs.event;

const capturedScenarios = [];

Before(test => {
capturedScenarios.push({
phase: 'Before-hook',
title: test && test.title,
tags: test && Array.isArray(test.tags) ? [...test.tags] : null,
});
});

After(test => {
capturedScenarios.push({
phase: 'After-hook',
title: test && test.title,
tags: test && Array.isArray(test.tags) ? [...test.tags] : null,
});
});

event.dispatcher.on(event.test.before, test => {
capturedScenarios.push({
phase: 'event.test.before',
title: test && test.title,
tags: test && Array.isArray(test.tags) ? [...test.tags] : null,
});
});

event.dispatcher.on(event.test.after, test => {
capturedScenarios.push({
phase: 'event.test.after',
title: test && test.title,
tags: test && Array.isArray(test.tags) ? [...test.tags] : null,
});
});

Given('I opened website', () => {
// From "gherkin/basic.feature" {"line":8,"column":5}
I.amOnPage('/');
});

Then('the Before hook should have captured this scenario', () => {
const captureSnapshot = JSON.stringify(capturedScenarios, null, 2);

const matchesScenario = entry =>
entry.tags && entry.tags.includes('@scenarioHook') && entry.tags.includes('@hookCapture');

const bddBefore = capturedScenarios.find(
entry => entry.phase === 'Before-hook' && matchesScenario(entry),
);
const eventBefore = capturedScenarios.find(
entry => entry.phase === 'event.test.before' && matchesScenario(entry),
);

if (!bddBefore) throw new Error(`BDD Before() did not fire for @scenarioHook. Captured:\n${captureSnapshot}`);
if (!eventBefore) throw new Error(`event.test.before did not fire with real test. Captured:\n${captureSnapshot}`);

for (const entry of [bddBefore, eventBefore]) {
if (!entry.title || entry.title === '...') {
throw new Error(`Placeholder title in ${entry.phase}: ${JSON.stringify(entry)}`);
}
if (!entry.title.includes('Before hook captures scenario metadata')) {
throw new Error(`Unexpected title in ${entry.phase}: ${JSON.stringify(entry)}`);
}
}
});

When('go to {string} page', (url) => {
// From "gherkin/basic.feature" {"line":9,"column":5}
I.amOnPage(url);
Expand Down
27 changes: 27 additions & 0 deletions test/data/sandbox/codecept.bdd.events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export const config = {
tests: './*_no_test.js',
timeout: 10000,
output: './output',
helpers: {
BDD: {
require: './support/bdd_helper.js',
},
},
gherkin: {
features: './features/*.feature',
steps: [
'./features/step_definitions/my_steps.js',
'./features/step_definitions/my_other_steps.js',
],
},
include: {},
bootstrap: false,
mocha: {},
plugins: {
captureTestBefore: {
require: './support/capture_test_before.js',
enabled: true,
},
},
name: 'sandbox',
}
31 changes: 31 additions & 0 deletions test/data/sandbox/support/capture_test_before.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from 'fs'
import event from '../../../../lib/event.js'

export default function (config) {
const captured = []

event.dispatcher.on(event.test.before, test => {
captured.push({
phase: 'test.before',
title: test?.title ?? null,
tags: Array.isArray(test?.tags) ? [...test.tags] : null,
})
})

event.dispatcher.on(event.test.after, test => {
captured.push({
phase: 'test.after',
title: test?.title ?? null,
tags: Array.isArray(test?.tags) ? [...test.tags] : null,
})
})

const flush = () => {
const outPath = config.outputFile || process.env.CAPTURE_OUTPUT
if (!outPath) return
fs.writeFileSync(outPath, JSON.stringify(captured, null, 2))
}

event.dispatcher.on(event.all.result, flush)
event.dispatcher.on(event.all.after, flush)
}
31 changes: 31 additions & 0 deletions test/runner/bdd_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as chai from 'chai'
chai.should()
import assert from 'assert'
import fs from 'fs'
import os from 'os'
import path from 'path'
import { exec } from 'child_process'
import { fileURLToPath } from 'url'
Expand Down Expand Up @@ -347,6 +349,35 @@ When(/^I define a step with a \\( paren and a "(.*?)" string$/, () => {
})
})

it('emits event.test.before/after with the real scenario title and tags', done => {
const captureFile = path.join(os.tmpdir(), `codeceptjs-capture-${process.pid}-${Date.now()}.json`)
const cmd = `${config_run_config('codecept.bdd.events.js')} --grep "@important"`
exec(cmd, { env: { ...process.env, CAPTURE_OUTPUT: captureFile } }, (err, stdout, stderr) => {
try {
assert(!err, `runner failed: ${stderr || stdout}`)
const captured = JSON.parse(fs.readFileSync(captureFile, 'utf8'))

const beforeEvents = captured.filter(e => e.phase === 'test.before')
const afterEvents = captured.filter(e => e.phase === 'test.after')

beforeEvents.length.should.be.greaterThan(0)
afterEvents.length.should.be.greaterThan(0)

for (const entry of [...beforeEvents, ...afterEvents]) {
entry.title.should.not.equal('...')
entry.title.should.include('checkout')
entry.tags.should.include('@important')
entry.tags.should.include('@very')
}
done()
} catch (assertErr) {
done(assertErr)
} finally {
try { fs.unlinkSync(captureFile) } catch {}
}
})
})

describe('i18n', () => {
const codecept_dir = path.join(__dirname, '/../data/sandbox/i18n')
const config_run_config = config => `${codecept_run} --config ${codecept_dir}/${config}`
Expand Down
157 changes: 156 additions & 1 deletion test/unit/bdd_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const builder = new Gherkin.AstBuilder(uuidFn)
const matcher = new Gherkin.GherkinClassicTokenMatcher()

import Config from '../../lib/config.js'
import { Given, When, And, Then, matchStep, clearSteps, defineParameterType } from '../../lib/mocha/bdd.js'
import { Given, When, And, Then, Before, After, matchStep, clearSteps, defineParameterType } from '../../lib/mocha/bdd.js'
import run from '../../lib/mocha/gherkin.js'
import recorder from '../../lib/recorder.js'
import container from '../../lib/container.js'
Expand Down Expand Up @@ -441,4 +441,159 @@ describe('BDD', () => {
expect('blue').is.equal(color.name)
await Promise.resolve()
})

describe('Gherkin hook events', () => {
const featureText = `
@feature_tag
Feature: checkout flow

@scenario_tag
Scenario: buy a product
Given I have product with 600 price
When I go to checkout process
`

const registerBasicSteps = () => {
Given(/I have product with (\d+) price/, () => {})
When('I go to checkout process', () => {})
}

const removeListeners = events => {
for (const name of events) event.dispatcher.removeAllListeners(name)
}

const driveHook = (hook, currentTest) =>
new Promise((resolve, reject) => {
hook.fn.call({ currentTest }, err => (err ? reject(err) : resolve()))
})

it('event.test.before carries the real scenario title and tags', async () => {
registerBasicSteps()
const suite = await run(featureText)
const captured = []
event.dispatcher.on(event.test.before, t => captured.push({ title: t.title, tags: t.tags }))

try {
const beforeHook = suite._beforeEach.find(h => h.title.includes('codeceptjs.before'))
await driveHook(beforeHook, suite.tests[0])

expect(captured).to.have.lengthOf(1)
expect(captured[0].title).to.equal('buy a product @scenario_tag')
expect(captured[0].tags).to.include.members(['@feature_tag', '@scenario_tag'])
expect(captured[0].title).to.not.equal('...')
} finally {
removeListeners([event.test.before])
}
})

it('event.test.after carries the real scenario title and tags', async () => {
registerBasicSteps()
const suite = await run(featureText)
const captured = []
event.dispatcher.on(event.test.after, t => captured.push({ title: t.title, tags: t.tags }))

try {
const afterHook = suite._afterEach.find(h => h.title.includes('codeceptjs.after'))
await driveHook(afterHook, suite.tests[0])

expect(captured).to.have.lengthOf(1)
expect(captured[0].title).to.equal('buy a product @scenario_tag')
expect(captured[0].tags).to.include.members(['@feature_tag', '@scenario_tag'])
expect(captured[0].title).to.not.equal('...')
} finally {
removeListeners([event.test.after])
}
})

it('forwards the done callback from setup() and emits before invoking it', async () => {
registerBasicSteps()
const suite = await run(featureText)
const beforeHook = suite._beforeEach.find(h => h.title.includes('codeceptjs.before'))
const order = []
event.dispatcher.on(event.test.before, () => order.push('emitted'))

try {
let doneCalls = 0
await new Promise((resolve, reject) => {
beforeHook.fn.call({ currentTest: suite.tests[0] }, err => {
doneCalls++
order.push('done')
err ? reject(err) : resolve()
})
})
expect(doneCalls).to.equal(1)
expect(order).to.deep.equal(['emitted', 'done'])
} finally {
removeListeners([event.test.before])
}
})

it('Before(test => ...) step-definition hook receives the real scenario', async () => {
const seen = []
Before(test => seen.push({ title: test.title, tags: test.tags }))
registerBasicSteps()
const suite = await run(featureText)

try {
await new Promise((resolve, reject) => {
suite.tests[0].fn(err => (err ? reject(err) : resolve()))
})

expect(seen).to.have.lengthOf.at.least(1)
const recorded = seen[seen.length - 1]
expect(recorded.title).to.equal('buy a product @scenario_tag')
expect(recorded.tags).to.include.members(['@feature_tag', '@scenario_tag'])
} finally {
removeListeners([event.test.started])
}
})

it('emits test.before, test.started, test.after in the expected order', async () => {
registerBasicSteps()
const suite = await run(featureText)
const order = []
const record = name => () => order.push(name)
event.dispatcher.on(event.test.before, record('test.before'))
event.dispatcher.on(event.test.started, record('test.started'))
event.dispatcher.on(event.test.passed, record('test.passed'))
event.dispatcher.on(event.test.after, record('test.after'))

try {
const beforeHook = suite._beforeEach.find(h => h.title.includes('codeceptjs.before'))
const afterHook = suite._afterEach.find(h => h.title.includes('codeceptjs.after'))

await driveHook(beforeHook, suite.tests[0])
await new Promise((resolve, reject) => {
suite.tests[0].fn(err => (err ? reject(err) : resolve()))
})
await driveHook(afterHook, suite.tests[0])

expect(order.indexOf('test.before')).to.be.lessThan(order.indexOf('test.started'))
expect(order.indexOf('test.started')).to.be.lessThan(order.indexOf('test.passed'))
expect(order.indexOf('test.passed')).to.be.lessThan(order.indexOf('test.after'))
} finally {
removeListeners([event.test.before, event.test.started, event.test.passed, event.test.after])
}
})

it('After(test => ...) step-definition hook receives the real scenario', async () => {
const seen = []
After(test => seen.push({ title: test.title, tags: test.tags }))
registerBasicSteps()
const suite = await run(featureText)

try {
await new Promise((resolve, reject) => {
suite.tests[0].fn(err => (err ? reject(err) : resolve()))
})

expect(seen).to.have.lengthOf.at.least(1)
const recorded = seen[seen.length - 1]
expect(recorded.title).to.equal('buy a product @scenario_tag')
expect(recorded.tags).to.include.members(['@feature_tag', '@scenario_tag'])
} finally {
removeListeners([event.test.finished])
}
})
})
})