Skip to content
This repository has been archived by the owner on Nov 29, 2023. It is now read-only.

Commit

Permalink
feat: add support for Feature Contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Mar 2, 2021
1 parent 7cfcdb6 commit ef33827
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 7 deletions.
23 changes: 23 additions & 0 deletions src/lib/contextualizeFeature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ContextualizedFeature, SkippableFeature } from './load-features'
import { parseContexts } from './parseContexts'

/**
* If a "Contexts" table is present, run the feature for every context
*/
export const contextualizeFeature = (
feature: SkippableFeature,
): ContextualizedFeature[] => {
const { description } = feature
const contexts = parseContexts(description ?? '')
if (contexts.length === 0)
return [
{
...feature,
context: {},
},
]
return contexts.map((context) => ({
...feature,
context,
}))
}
4 changes: 4 additions & 0 deletions src/lib/load-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export type SkippableFeature = cucumber.GherkinDocument.IFeature & {
dependsOn: cucumber.GherkinDocument.IFeature[]
}

export type ContextualizedFeature = SkippableFeature & {
context: Record<string, string>
}

export const parseFeatures = (featureData: Buffer[]): SkippableFeature[] => {
const parsedFeatures = featureData.map((d) => {
// Parse the feature files
Expand Down
36 changes: 36 additions & 0 deletions src/lib/parseContexts.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { parseContexts } from './parseContexts'

const descriptionWithContexts = `If a feature has a contexts table,
it should be repeated for all the entries.
This allows to use contexts over multiple scenarios which is useful when
scenarios need to be split up so that a retries do not trigger execution
of a previous scenario.
Note: "Context" has been chosen to distinguish it from the Gherkin "Scenario Outline + Examples"
which is very similar, but is applied per scenario.
Contexts:
| arg1 | arg2 |
| Hello | World |
| ¡Hola! | Mundo |`

const descriptionWithoutContexts = `No contexts here.`

describe('parseContextss', () => {
it('should parse contexts', () => {
expect(parseContexts(descriptionWithContexts)).toEqual([
{
arg1: 'Hello',
arg2: 'World',
},
{
arg1: '¡Hola!',
arg2: 'Mundo',
},
])
})
it('should ignore if contexts do not exist', () => {
expect(parseContexts(descriptionWithoutContexts)).toEqual([])
})
})
39 changes: 39 additions & 0 deletions src/lib/parseContexts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const tableMatch = /^\|([^|]+\|)+$/

/**
* Parse a Contexts definition into a table
*/
export const parseContexts = (
description: string,
): Record<string, string>[] => {
const lines = description.split('\n').map((s) => s.trim())
if (lines.find((s) => s === 'Contexts:') === undefined) return []

const table = []
let contextsFound = false
// only pick up table lines immediately following the Contexts: token
for (let line = 0; line < lines.length; line++) {
if (lines[line] === 'Contexts:' && lines[line + 1] === '') {
contextsFound = true
continue
}
if (!contextsFound) continue
if (lines[line] === '') continue
if (tableMatch.exec(lines[line]) === null) {
break
}
table.push(
lines[line]
.substr(1, lines[line].length - 2)
.split('|')
.map((s) => s.trim()),
)
}
const [headers, ...rest] = table
return rest.map((line) =>
headers.reduce(
(row, header, k) => ({ ...row, [header]: line[k] }),
{} as Record<string, string>,
),
)
}
26 changes: 26 additions & 0 deletions src/lib/runner.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as path from 'path'
import { regexGroupMatcher } from './regexGroupMatcher'
import { FeatureRunner } from './runner'

describe('runner', () => {
Expand All @@ -23,4 +24,29 @@ describe('runner', () => {
})
})
})
describe('Feature contexts', () => {
it('should run a scenario for every context if given', async () => {
const logs: string[] = []
const result = await new FeatureRunner(
{},
{
dir: path.join(process.cwd(), 'test', 'feature-contexts'),
retry: false,
},
)
.addStepRunners([
regexGroupMatcher(/I log "(?<valueName>[^"]+)"/)(
async ({ valueName }) => {
logs.push(valueName)
return valueName
},
),
])
.run()

expect(result.success).toEqual(true)
expect(result.featureResults).toHaveLength(2)
expect(logs).toEqual(['Hello World', '¡Hola! Mundo'])
})
})
})
56 changes: 49 additions & 7 deletions src/lib/runner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { fromDirectory, SkippableFeature } from './load-features'
import {
ContextualizedFeature,
fromDirectory,
SkippableFeature,
} from './load-features'
import { ConsoleReporter } from './console-reporter'
import { exponential } from 'backoff'
import { messages as cucumber } from 'cucumber-messages'
import { replaceStoragePlaceholders } from './replaceStoragePlaceholders'
import { retryConfiguration, RetryConfiguration } from './retryConfiguration'
import { contextualizeFeature } from './contextualizeFeature'

const allSuccess = (r: boolean, result: Result) => (result.success ? r : false)

Expand Down Expand Up @@ -88,7 +93,15 @@ export class FeatureRunner<W extends Store> {
success: false,
})
} else {
featureResults.push(await this.runFeature(feature))
await contextualizeFeature(feature).reduce(
(p, contextualizedFeature) =>
p.then(() =>
this.runFeature(contextualizedFeature).then((res) => {
featureResults.push(res)
}),
),
Promise.resolve(),
)
}
}),
Promise.resolve(),
Expand All @@ -110,7 +123,7 @@ export class FeatureRunner<W extends Store> {
return result
}

async runFeature(feature: SkippableFeature): Promise<FeatureResult> {
async runFeature(feature: ContextualizedFeature): Promise<FeatureResult> {
await this.progress('feature', `${feature.name}`)
if (feature.skip) {
return {
Expand All @@ -125,6 +138,16 @@ export class FeatureRunner<W extends Store> {
flags: {},
settings: {},
} as const

// Replace contexts
const applyContext = (step?: string | null): string | undefined => {
if (step === undefined || step === null) return undefined
let replaced = step
for (const [k, v] of Object.entries(feature.context)) {
replaced = replaced.replace(`<${k}>`, v)
}
return replaced
}
await feature?.children?.reduce(
(promise, scenario) =>
promise.then(async () => {
Expand Down Expand Up @@ -167,11 +190,13 @@ export class FeatureRunner<W extends Store> {
name: `${scenario.scenario?.name} (${values?.join(',')})`,
steps: scenario.scenario?.steps?.map((step) => ({
...step,
text: replace(step.text ?? ''),
text: applyContext(replace(step.text ?? '')),
docString: step.docString
? {
...step.docString,
content: replace(step.docString.content ?? ''),
content: applyContext(
replace(step.docString.content ?? ''),
),
}
: undefined,
})),
Expand All @@ -190,8 +215,25 @@ export class FeatureRunner<W extends Store> {
)
}
} else {
const s = scenario.scenario ?? scenario.background
if (s) {
const useScenario = scenario.scenario ?? scenario.background

if (useScenario !== undefined && useScenario !== null) {
const s:
| cucumber.GherkinDocument.Feature.IScenario
| cucumber.GherkinDocument.Feature.IBackground = {
...useScenario,
}
// Replace contexts
s.steps = [
...(s?.steps?.map((s) => ({
...s,
text: applyContext(s.text),
docString: {
...s.docString,
content: applyContext(s.docString?.content),
},
})) ?? []),
]
if (this.retry) {
scenarioResults.push(
await this.retryScenario(s, flightRecorder),
Expand Down
20 changes: 20 additions & 0 deletions test/feature-contexts/FeatureContexts.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Feature Contexts

If a feature has a contexts table,
it should be repeated for all the entries.
This allows to use contexts over multiple scenarios which is useful when
scenarios need to be split up so that a retries do not trigger execution
of a previous scenario.

Note: "Context" has been chosen to distinguish it from the Gherkin "Scenario Outline + Examples"
which is very similar, but is applied per scenario.

Contexts:

| arg1 | arg2 |
| Hello | World |
| ¡Hola! | Mundo |

Scenario:

Given I log "<arg1> <arg2>"

0 comments on commit ef33827

Please sign in to comment.