From db82746db0b5a58f4e1d175048218dfdb6cd713e Mon Sep 17 00:00:00 2001 From: David Hunt Date: Sat, 9 Nov 2024 03:20:21 +1300 Subject: [PATCH] fix(main): problems with Scenario Outline rendering --- .changeset/dull-balloons-punch.md | 11 +++++++++++ packages/main/gherkin-example/example.feature | 14 +++++++------- .../main/gherkin-example/example.feature.js | 18 +++++++++--------- packages/main/src/render.ts | 15 +++++++-------- packages/main/tests/renderer.test.ts | 18 ++++++++++++------ 5 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 .changeset/dull-balloons-punch.md diff --git a/.changeset/dull-balloons-punch.md b/.changeset/dull-balloons-punch.md new file mode 100644 index 0000000..df9e149 --- /dev/null +++ b/.changeset/dull-balloons-punch.md @@ -0,0 +1,11 @@ +--- +"quickpickle": patch +--- + +Fixed problems with Scenario Outline rendering; under the following conditions, +and probably some others, the renderer would fail. + +- If a parameter were named "context" +- Any regex characters (e.g. *) in a parameter name +- Having a ton of examples +- If a parameter name started with a number \ No newline at end of file diff --git a/packages/main/gherkin-example/example.feature b/packages/main/gherkin-example/example.feature index 610c979..2766ab1 100644 --- a/packages/main/gherkin-example/example.feature +++ b/packages/main/gherkin-example/example.feature @@ -17,16 +17,16 @@ Feature: QuickPickle's Comprehensive Gherkin Syntax Example Then a verifiable outcome is achieved' @concurrent - Scenario Outline: Parameterized scenario for , '', "" + Scenario Outline: Parameterized scenario for , '<5>', "" Given a 'precondition' with - When an "action" is taken with - Then the `outcome` is + When an "action" is taken with <5> + Then the `outcome` is Examples: - | parameter | another_parameter | expected_result | - | value1' | value2' | result1' | - | value3` | value4` | result2` | - | value5" | value6" | result3" | + | parameter | 5 | expected_result* | + | value1' | value2' | result1' | + | value3` | value4` | result2` | + | value5" | value6" | result3" | @data_table Scenario: Scenario with various DataTable types diff --git a/packages/main/gherkin-example/example.feature.js b/packages/main/gherkin-example/example.feature.js index a54adba..e9034ff 100644 --- a/packages/main/gherkin-example/example.feature.js +++ b/packages/main/gherkin-example/example.feature.js @@ -49,16 +49,16 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { }); test.concurrent.for([ - {"parameter":"value1'","another_parameter":"value2'","expected_result":"result1'"}, - {"parameter":"value3`","another_parameter":"value4`","expected_result":"result2`"}, - {"parameter":"value5\"","another_parameter":"value6\"","expected_result":"result3\""} + {"_0":"value1'","_1":"value2'","_2":"result1'"}, + {"_0":"value3`","_1":"value4`","_2":"result2`"}, + {"_0":"value5\"","_1":"value6\"","_2":"result3\""} ])( - 'Scenario Outline: Parameterized scenario for $parameter, \'$another_parameter\', "$expected_result" (@tag @multiple_tags @concurrent)', - async ({ parameter, another_parameter, expected_result }, context) => { - let state = await initScenario(context, `Parameterized scenario for ${parameter}, '${another_parameter}', "${expected_result}"`, ['@tag', '@multiple_tags', '@concurrent'], [`a 'precondition' with ${parameter}`,`an "action" is taken with ${another_parameter}`,`the \`outcome\` is ${expected_result}`]); - await gherkinStep(`a 'precondition' with ${parameter}`, state, 21, 1); - await gherkinStep(`an "action" is taken with ${another_parameter}`, state, 22, 2); - await gherkinStep(`the \`outcome\` is ${expected_result}`, state, 23, 3); + 'Scenario Outline: Parameterized scenario for $_0, \'$_1\', "$_2" (@tag @multiple_tags @concurrent)', + async ({ _0, _1, _2 }, context) => { + let state = await initScenario(context, `Parameterized scenario for ${_0}, '${_1}', "${_2}"`, ['@tag', '@multiple_tags', '@concurrent'], [`a 'precondition' with ${_0}`,`an "action" is taken with ${_1}`,`the \`outcome\` is ${_2}`]); + await gherkinStep(`a 'precondition' with ${_0}`, state, 21, 1); + await gherkinStep(`an "action" is taken with ${_1}`, state, 22, 2); + await gherkinStep(`the \`outcome\` is ${_2}`, state, 23, 3); await afterScenario(state); } ); diff --git a/packages/main/src/render.ts b/packages/main/src/render.ts index 15a42e9..f584309 100644 --- a/packages/main/src/render.ts +++ b/packages/main/src/render.ts @@ -3,7 +3,7 @@ import { type QuickPickleConfig } from '.' import * as Gherkin from '@cucumber/gherkin'; import * as Messages from '@cucumber/messages'; -import { fromPairs, intersection, pick } from "lodash-es"; +import { fromPairs, intersection, pick, escapeRegExp } from "lodash-es"; const uuidFn = Messages.IdGenerator.uuid(); const builder = new Gherkin.AstBuilder(uuidFn); @@ -156,14 +156,14 @@ function renderScenario(child:FeatureChild, config:QuickPickleConfig, tags:strin // For Scenario Outlines with examples if (child.scenario!.examples?.[0]?.tableHeader && child.scenario!.examples?.[0]?.tableBody) { - let paramNames = child.scenario?.examples?.[0].tableHeader?.cells?.map(c => c.value) || [] + let origParamNames = child.scenario?.examples?.[0]?.tableHeader?.cells?.map(c => c.value) || [] let paramValues = child.scenario?.examples?.[0].tableBody.map((r) => { - return fromPairs(r.cells.map((c,i) => [ paramNames![i], c.value ])) + return fromPairs(r.cells.map((c,i) => [ '_'+i, c.value ])) }) function replaceParamNames(t:string, withBraces?:boolean) { - paramNames.forEach(p => { - t = t.replace(new RegExp(`<${p}>`, 'g'), (withBraces ? `$\{${p}\}` : `$${p}`)) + origParamNames.forEach((p,i) => { + t = t.replace(new RegExp(`<${escapeRegExp(p)}>`, 'g'), (withBraces ? `$\{_${i}\}` : `$_${i}`)) }) return t } @@ -183,11 +183,10 @@ ${sp} ${paramValues?.map(line => { }).join(',\n' + sp + ' ')} ${sp}])( ${sp} '${q(child.scenario?.keyword || '')}: ${describe}${tagTextForVitest}', -${sp} async ({ ${paramNames?.join(', ')} }, context) => { +${sp} async ({ ${origParamNames.map((p,i) => '_'+i)?.join(', ')} }, context) => { ${sp} let state = await ${initFn}(context, \`${name}\`, ['${tags.join("', '") || ''}'], [${examples?.map(s => '`'+s+'`').join(',')}]); ${child.scenario?.steps.map((step,idx) => { - let text = step.text.replace(/`/g, '\\`') - text = replaceParamNames(text,true) + let text = replaceParamNames(step.text,true).replace(/`/g, '\\`') return `${sp} await gherkinStep(\`${text}\`, state, ${step.location.line}, ${idx+1}${isExploded ? `, ${explodedIdx + 1}` : ''});` }).join('\n') } diff --git a/packages/main/tests/renderer.test.ts b/packages/main/tests/renderer.test.ts index 5e9da97..fcca637 100644 --- a/packages/main/tests/renderer.test.ts +++ b/packages/main/tests/renderer.test.ts @@ -1,11 +1,17 @@ -import { expect, test } from 'vitest' +import { expect, test, describe } from 'vitest' import { renderGherkin } from '../src/render' import { defaultConfig, normalizeTags } from '../src' import fs from 'node:fs' -const featureFile = fs.readFileSync(__dirname + '/../gherkin-example/example.feature', 'utf8') -const jsFile = fs.readFileSync(__dirname + '/../gherkin-example/example.feature.js', 'utf8') - test('rendering the example feature file', () => { - expect(jsFile).toEqual(renderGherkin(featureFile, {...defaultConfig, failTags: normalizeTags('fail, fails'), explodeTags: [normalizeTags('1a,1b'),normalizeTags('2a,2b')] })) -}) \ No newline at end of file + const featureFile = fs.readFileSync(__dirname + '/../gherkin-example/example.feature', 'utf8') + const jsFile = fs.readFileSync(__dirname + '/../gherkin-example/example.feature.js', 'utf8') + const testJs = renderGherkin(featureFile, {...defaultConfig, failTags: normalizeTags('fail, fails'), explodeTags: [normalizeTags('1a,1b'),normalizeTags('2a,2b')] }) + try { + expect(jsFile).toEqual(testJs) + } + catch(e) { + fs.writeFileSync(__dirname + '/../gherkin-example/example.feature.diff.js', testJs) + throw e + } +})