codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
231 lines (208 loc) • 7.43 kB
JavaScript
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
import { IdGenerator } from '@cucumber/messages'
import { Context, Suite } from 'mocha'
import debug from 'debug'
const debugBdd = debug('codeceptjs:bdd')
import event from '../event.js'
import { injected, setup, teardown, suiteSetup, suiteTeardown } from './asyncWrapper.js'
import step from '../step/base.js'
import MetaStep from '../step/meta.js'
import DataTableArgument from '../data/dataTableArgument.js'
import transform from '../transform.js'
import { enhanceMochaSuite } from './suite.js'
import { createTest } from './test.js'
import { matchStep } from './bdd.js'
const uuidFn = IdGenerator.uuid()
const builder = new AstBuilder(uuidFn)
const matcher = new GherkinClassicTokenMatcher()
const parser = new Parser(builder, matcher)
parser.stopAtFirstError = false
const gherkinParser = (text, file) => {
const ast = parser.parse(text)
let currentLanguage
if (ast.feature) {
// Ensure translations are loaded before trying to access them
currentLanguage = getTranslation(ast.feature.language)
}
if (!ast.feature) {
throw new Error(`No 'Features' available in Gherkin '${file}' provided!`)
}
const suite = new Suite(ast.feature.name, new Context())
enhanceMochaSuite(suite)
const tags = ast.feature.tags.map(t => t.name)
suite.title = `${suite.title} ${tags.join(' ')}`.trim()
suite.tags = tags || []
suite.comment = ast.feature.description
suite.feature = ast.feature
suite.file = file
suite.timeout(0)
suite.beforeEach('codeceptjs.before', function (done) {
// In Mocha, 'this' is the hook Context; currentTest is the running scenario
setup(this)(done)
})
suite.afterEach('codeceptjs.after', function (done) {
teardown(this)(done)
})
suite.beforeAll('codeceptjs.beforeSuite', suiteSetup(suite))
suite.afterAll('codeceptjs.afterSuite', suiteTeardown(suite))
const runSteps = async steps => {
for (const step of steps) {
const metaStep = new MetaStep(null, step.text)
metaStep.actor = step.keyword.trim()
let helperStep
const setMetaStep = step => {
helperStep = step
if (step.metaStep) {
if (step.metaStep === metaStep) {
return
}
setMetaStep(step.metaStep)
return
}
step.metaStep = metaStep
}
const fn = matchStep(step.text)
if (step.dataTable) {
fn.params.push({
...step.dataTable,
parse: () => new DataTableArgument(step.dataTable),
})
metaStep.comment = `\n${transformTable(step.dataTable)}`
}
if (step.docString) {
fn.params.push(step.docString)
metaStep.comment = `\n"""\n${step.docString.content}\n"""`
}
step.startTime = Date.now()
step.match = fn.line
event.emit(event.bddStep.before, step)
event.emit(event.bddStep.started, metaStep)
event.dispatcher.prependListener(event.step.before, setMetaStep)
try {
debug(`Step '${step.text}' started...`)
await fn(...fn.params)
debug('Step passed')
step.status = 'passed'
} catch (err) {
debug(`Step failed: ${err?.message}`)
step.status = 'failed'
step.err = err
throw err
} finally {
step.endTime = Date.now()
event.dispatcher.removeListener(event.step.before, setMetaStep)
}
event.emit(event.bddStep.finished, metaStep)
event.emit(event.bddStep.after, step)
}
}
for (const child of ast.feature.children) {
if (child.background) {
suite.beforeEach(
'Before',
injected(async () => runSteps(child.background.steps), suite, 'before'),
)
continue
}
if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline === child.scenario.keyword : child.scenario.keyword === 'Scenario Outline')) {
for (const examples of child.scenario.examples) {
const fields = examples.tableHeader.cells.map(c => c.value)
for (const example of examples.tableBody) {
let exampleSteps = [...child.scenario.steps]
const current = {}
for (const index in example.cells) {
const placeholder = fields[index]
const value = transform('gherkin.examples', example.cells[index].value)
example.cells[index].value = value
current[placeholder] = value
exampleSteps = exampleSteps.map(step => {
step = { ...step }
step.text = step.text.split(`<${placeholder}>`).join(value)
return step
})
}
const tags = child.scenario.tags.map(t => t.name).concat(examples.tags.map(t => t.name))
let title = `${child.scenario.name} ${JSON.stringify(current)} ${tags.join(' ')}`.trim()
for (const [key, value] of Object.entries(current)) {
if (title.includes(`<${key}>`)) {
title = title.replace(JSON.stringify(current), '').replace(`<${key}>`, value)
}
}
const test = createTest(title, async () => runSteps(addExampleInTable(exampleSteps, current)))
test.addToSuite(suite)
test.tags = suite.tags.concat(tags)
test.file = file
}
}
continue
}
if (child.scenario) {
const tags = child.scenario.tags.map(t => t.name)
const title = `${child.scenario.name} ${tags.join(' ')}`.trim()
const test = createTest(title, async () => runSteps(child.scenario.steps))
test.addToSuite(suite)
test.tags = suite.tags.concat(tags)
test.file = file
}
}
return suite
}
function transformTable(table) {
let str = ''
for (const id in table.rows) {
const cells = table.rows[id].cells
str += cells
.map(c => c.value)
.map(c => c.padEnd(15))
.join(' | ')
str += '\n'
}
return str
}
function addExampleInTable(exampleSteps, placeholders) {
const steps = JSON.parse(JSON.stringify(exampleSteps))
for (const placeholder in placeholders) {
steps.map(step => {
step = { ...step }
if (step.dataTable) {
for (const id in step.dataTable.rows) {
const cells = step.dataTable.rows[id].cells
cells.map(c => (c.value = c.value.replace(`<${placeholder}>`, placeholders[placeholder])))
}
}
return step
})
}
return steps
}
// Import translations at module level to avoid async in parser
let translations = null
async function loadTranslations() {
if (!translations) {
// Import container to ensure it's initialized
const Container = await import('../container.js')
await Container.default.started()
// Now load translations
const translationsModule = await import('../../translations/index.js')
translations = translationsModule.default || translationsModule
}
return translations
}
function getTranslation(language) {
if (!translations) {
// Translations not loaded yet, return null (will use default)
return null
}
const translationKeys = Object.keys(translations)
for (const availableTranslation of translationKeys) {
if (!language) {
break
}
if (availableTranslation.includes(language)) {
return translations[availableTranslation]
}
}
return null
}
export { loadTranslations }
export default gherkinParser