UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

231 lines (208 loc) 7.43 kB
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