From d4d57175fef1e87dde4d5e9e6ed7626e8a35fae4 Mon Sep 17 00:00:00 2001 From: David Hunt Date: Tue, 17 Dec 2024 20:06:51 +1300 Subject: [PATCH] fix: DocStrings and DataTables replacements in Scenario Outlines Refs: Fixes #23 --- .changeset/smart-news-march.md | 5 ++ packages/main/gherkin-example/example.feature | 26 ++++++++++ .../main/gherkin-example/example.feature.js | 52 +++++++++++++++---- packages/main/src/render.ts | 14 +++-- packages/main/tests/test.feature | 24 +++++++-- packages/main/tests/tests.steps.ts | 17 +++--- 6 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 .changeset/smart-news-march.md diff --git a/.changeset/smart-news-march.md b/.changeset/smart-news-march.md new file mode 100644 index 0000000..bab031a --- /dev/null +++ b/.changeset/smart-news-march.md @@ -0,0 +1,5 @@ +--- +"quickpickle": patch +--- + +Fix DataTables and DocStrings replacements in Scenario Outlines diff --git a/packages/main/gherkin-example/example.feature b/packages/main/gherkin-example/example.feature index 5be1ee3..e45d030 100644 --- a/packages/main/gherkin-example/example.feature +++ b/packages/main/gherkin-example/example.feature @@ -152,3 +152,29 @@ Feature: QuickPickle's Comprehensive Gherkin Syntax Example Given a "string with \"quotes\"" And `someone` is ${sneaky} \${with} \\${backslashes} \\\${and} \$\{other} \`things\\` 'like' \'quotes\\' + Scenario Outline: DataTables row: + Given the following datatable: + | Product | Quantity | + | | | + | | | + Then datatable should contain "" + + Examples: + | Row | Product1 | Qty1 | Product2 | Qty2 | + | 0 | Widget A | 2 | Widget B | 3 | + | 1 | Widget C | 1 | Widget D | 4 | + + Scenario Outline: DocStrings row: + And the following json: + """ + { + "": "", + "": "" + } + """ + Then json should contain "" + + Examples: + | Row | Product1 | Qty1 | Product2 | Qty2 | + | 0 | Widget A | 2 | Widget B | 3 | + | 1 | Widget C | 1 | Widget D | 4 | diff --git a/packages/main/gherkin-example/example.feature.js b/packages/main/gherkin-example/example.feature.js index 55e5553..836b107 100644 --- a/packages/main/gherkin-example/example.feature.js +++ b/packages/main/gherkin-example/example.feature.js @@ -65,11 +65,11 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { test('Scenario: Scenario with various DataTable types (@tag @multiple_tags @data_table)', async (context) => { let state = await initScenario(context, 'Scenario with various DataTable types', ['@tag', '@multiple_tags', '@data_table'], [`a list of strings:`,`a list of integers:`,`a map of string to string:`,`a list of maps:`,`a map of string to list of string:`,`they are processed`,`the system behaves correctly`]); - await gherkinStep('Context', `a list of strings:`, state, 33, 1, undefined, [["Apple'"],["Banana`"],["Cherry\""]]); - await gherkinStep('Context', `a list of integers:`, state, 37, 2, undefined, [["1"],["2"],["3"]]); - await gherkinStep('Context', `a map of string to string:`, state, 41, 3, undefined, [["key1'","value1'"],["key2`","value2\""]]); - await gherkinStep('Context', `a list of maps:`, state, 44, 4, undefined, [["name'","age`","role\""],["Alice'","30","admin\""],["Bob`","25","user\""]]); - await gherkinStep('Context', `a map of string to list of string:`, state, 48, 5, undefined, [["fruits","Apple, Banana, Cherry"],["vegetables","Carrot, Potato, Onion"]]); + await gherkinStep('Context', `a list of strings:`, state, 33, 1, undefined, [[`Apple'`],[`Banana\``],[`Cherry"`]]); + await gherkinStep('Context', `a list of integers:`, state, 37, 2, undefined, [[`1`],[`2`],[`3`]]); + await gherkinStep('Context', `a map of string to string:`, state, 41, 3, undefined, [[`key1'`,`value1'`],[`key2\``,`value2"`]]); + await gherkinStep('Context', `a list of maps:`, state, 44, 4, undefined, [[`name'`,`age\``,`role"`],[`Alice'`,`30`,`admin"`],[`Bob\``,`25`,`user"`]]); + await gherkinStep('Context', `a map of string to list of string:`, state, 48, 5, undefined, [[`fruits`,`Apple, Banana, Cherry`],[`vegetables`,`Carrot, Potato, Onion`]]); await gherkinStep('Action', `they are processed`, state, 51, 6); await gherkinStep('Outcome', `the system behaves correctly`, state, 52, 7); await afterScenario(state); @@ -104,7 +104,9 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { test.todo.skip('Scenario: Scenario with doc string (@tag @multiple_tags @rule_tag @wip @skip)', async (context) => { let state = await initRuleScenario(context, 'Scenario with doc string', ['@tag', '@multiple_tags', '@rule_tag', '@wip', '@skip'], [`a document with the following content:`,`the document is processed`,`the system handles it correctly`]); - await gherkinStep('Context', `a document with the following content:`, state, 76, 1, undefined, {"content":"This is a doc string.\nIt can contain multiple lines.\nUseful for specifying larger text inputs."}); + await gherkinStep('Context', `a document with the following content:`, state, 76, 1, undefined, {content:`This is a doc string. +It can contain multiple lines. +Useful for specifying larger text inputs.`, mediaType:null }); await gherkinStep('Action', `the document is processed`, state, 82, 2); await gherkinStep('Outcome', `the system handles it correctly`, state, 83, 3); await afterScenario(state); @@ -112,7 +114,10 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { test('Scenario: Scenario with content type doc string (@tag @multiple_tags @rule_tag)', async (context) => { let state = await initRuleScenario(context, 'Scenario with content type doc string', ['@tag', '@multiple_tags', '@rule_tag'], [`a document with the following Markdown content:`]); - await gherkinStep('Context', `a document with the following Markdown content:`, state, 86, 1, undefined, {"content":"Lorem Ipsum\n===============\nLorem ipsum dolor sit amet,\nconsectetur adipiscing elit.","mediaType":"markdown"}); + await gherkinStep('Context', `a document with the following Markdown content:`, state, 86, 1, undefined, {content:`Lorem Ipsum +=============== +Lorem ipsum dolor sit amet, +consectetur adipiscing elit.`, mediaType:`markdown` }); await afterScenario(state); }); @@ -222,8 +227,8 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { async ({ _0, _1, _2 }, context) => { let state = await initRuleScenario(context, `Search Ordering: ${_0} (${_1}) ${_2}`, ['@tag', '@multiple_tags'], [`I search for "${_0}" and get results from 500 books`,`I should see search results with these metrics:`,`next I should see search results with these metrics:`]); await gherkinStep('Action', `I search for "${_0}" and get results from 500 books`, state, 129, 1); - await gherkinStep('Outcome', `I should see search results with these metrics:`, state, 130, 2, undefined, [["Book Importance","Match Quality","score"],["primary","exact","3+5=8"]]); - await gherkinStep('Outcome', `next I should see search results with these metrics:`, state, 133, 3, undefined, [["secondary","exact","2+5=7"]]); + await gherkinStep('Outcome', `I should see search results with these metrics:`, state, 130, 2, undefined, [[`Book Importance`,`Match Quality`,`score`],[`primary`,`exact`,`3+5=8`]]); + await gherkinStep('Outcome', `next I should see search results with these metrics:`, state, 133, 3, undefined, [[`secondary`,`exact`,`2+5=7`]]); await afterScenario(state); } ); @@ -249,6 +254,35 @@ describe('Feature: QuickPickle\'s Comprehensive Gherkin Syntax Example', () => { await afterScenario(state); }); + test.for([ + {"_0":"0","_1":"Widget A","_2":"2","_3":"Widget B","_4":"3"}, + {"_0":"1","_1":"Widget C","_2":"1","_3":"Widget D","_4":"4"} + ])( + 'Scenario Outline: DataTables row: $_0 (@tag @multiple_tags)', + async ({ _0, _1, _2, _3, _4 }, context) => { + let state = await initRuleScenario(context, `DataTables row: ${_0}`, ['@tag', '@multiple_tags'], [`the following datatable:`,`datatable should contain "${_1}"`]); + await gherkinStep('Context', `the following datatable:`, state, 156, 1, undefined, [[`Product`,`Quantity`],[`${_1}`,`${_2}`],[`${_3}`,`${_4}`]]); + await gherkinStep('Outcome', `datatable should contain "${_1}"`, state, 160, 2); + await afterScenario(state); + } + ); + + test.for([ + {"_0":"0","_1":"Widget A","_2":"2","_3":"Widget B","_4":"3"}, + {"_0":"1","_1":"Widget C","_2":"1","_3":"Widget D","_4":"4"} + ])( + 'Scenario Outline: DocStrings row: $_0 (@tag @multiple_tags)', + async ({ _0, _1, _2, _3, _4 }, context) => { + let state = await initRuleScenario(context, `DocStrings row: ${_0}`, ['@tag', '@multiple_tags'], [`the following json:`,`json should contain "${_1}"`]); + await gherkinStep('Context', `the following json:`, state, 168, 1, undefined, {content:`{ + "${_1}": "${_2}", + "${_3}": "${_4}" +}`, mediaType:null }); + await gherkinStep('Outcome', `json should contain "${_1}"`, state, 175, 2); + await afterScenario(state); + } + ); + }); diff --git a/packages/main/src/render.ts b/packages/main/src/render.ts index aee6176..3b320f6 100644 --- a/packages/main/src/render.ts +++ b/packages/main/src/render.ts @@ -4,7 +4,7 @@ import { tagsMatch, normalizeTags } from './tags' import * as Gherkin from '@cucumber/gherkin'; import * as Messages from '@cucumber/messages'; -import { fromPairs, pick, escapeRegExp } from "lodash-es"; +import { fromPairs, pick, escapeRegExp, replace } from "lodash-es"; const uuidFn = Messages.IdGenerator.uuid(); const builder = new Gherkin.AstBuilder(uuidFn); @@ -158,7 +158,7 @@ function renderScenario(child:FeatureChild, config:QuickPickleConfig, tags:strin return fromPairs(r.cells.map((c,i) => [ '_'+i, c.value ])) }) - function replaceParamNames(t:string, withBraces?:boolean) { + const replaceParamNames = (t:string, withBraces?:boolean) => { origParamNames.forEach((p,i) => { t = t.replace(new RegExp(`<${escapeRegExp(p)}>`, 'g'), (withBraces ? `$\{_${i}\}` : `$_${i}`)) }) @@ -173,7 +173,7 @@ function renderScenario(child:FeatureChild, config:QuickPickleConfig, tags:strin return text }) - let renderedSteps = renderSteps(child.scenario!.steps.map(s => ({...s, text: replaceParamNames(s.text, true)})), config, sp + ' ', isExploded ? `${explodedIdx+1}` : '') + let renderedSteps = renderSteps(child.scenario!.steps.map(s => ({...s, text: replaceParamNames(s.text, true)})), config, sp + ' ', isExploded ? `${explodedIdx+1}` : '', false, replaceParamNames) return ` ${sp}test${attrs}.for([ @@ -201,18 +201,16 @@ ${sp}}); }).join('\n\n') } -function renderSteps(steps:Step[], config:QuickPickleConfig, sp = ' ', explodedText = '', isBackground:boolean = false) { +function renderSteps(steps:Step[], config:QuickPickleConfig, sp = ' ', explodedText = '', isBackground:boolean = false, replaceParamNames:(t:string, withBraces?:boolean) => string = (t) => t) { let minus = isBackground ? '-' : '' return steps.map((step,idx) => { if (step.dataTable) { - let data = JSON.stringify(step.dataTable.rows.map(r => { - return r.cells.map(c => c.value) - })) + let data = `[${step.dataTable.rows.map(r => `[${r.cells.map(c => tl(replaceParamNames(c.value, true))).join(',')}]`).join(',')}]` return `${sp}await gherkinStep('${getStepType(steps, idx)}', ${tl(step.text)}, state, ${step.location.line}, ${minus}${idx+1}, ${explodedText || 'undefined'}, ${data});` } else if (step.docString) { - let data = JSON.stringify(pick(step.docString, ['content','mediaType'])) + let data = `{content:${tl(replaceParamNames(step.docString.content, true))}, mediaType:${step.docString?.mediaType ? tl(step.docString.mediaType) : 'null'} }` return `${sp}await gherkinStep('${getStepType(steps, idx)}', ${tl(step.text)}, state, ${step.location.line}, ${minus}${idx+1}, ${explodedText || 'undefined'}, ${data});` } diff --git a/packages/main/tests/test.feature b/packages/main/tests/test.feature index c24d874..67300dd 100644 --- a/packages/main/tests/test.feature +++ b/packages/main/tests/test.feature @@ -102,13 +102,26 @@ Feature: Basic Test | 5 | 8 | 13 | | 8 | 13 | 21 | | 13 | 21 | 34 | - - Scenario Outline: DataTables row: + + Scenario Outline: Replacements in strings work for : () + Given I set the data property "" to "" + Then the variable "data." should be "" + And the variable "data." should be characters long + + Examples: + | prop | value | length | + | foo | bar | 3 | + | fwah | fwee | 4 | + + Scenario Outline: DataTables row: Given the following datatable: | Product | Quantity | | | | | | | - Then datatable should contain "" + Then the datatable should contain "" + And the datatable should contain "" + And the datatable should contain "" + And the datatable should contain "" Examples: | Row | Product1 | Qty1 | Product2 | Qty2 | @@ -123,7 +136,10 @@ Feature: Basic Test "": "" } """ - Then json should contain "" + Then the json should contain "" + And the json should contain "" + And the json should contain "" + And the json should contain "" Examples: | Row | Product1 | Qty1 | Product2 | Qty2 | diff --git a/packages/main/tests/tests.steps.ts b/packages/main/tests/tests.steps.ts index 9389d0a..a7af6e7 100644 --- a/packages/main/tests/tests.steps.ts +++ b/packages/main/tests/tests.steps.ts @@ -49,19 +49,14 @@ Then('the sum should be {int}', (world, int) => { expect(world.numbers.reduce((a,b) => a + b, 0)).toBe(int) }) -Then("datatable should contain {string}", (world, value) => { - const values = world.datatable - .rows() - .reduce((prev, curr) => [...curr, ...prev], []); - expect(values.includes(value)).toBe(true); +Then("the datatable should contain {string}", (world, value) => { + const values = JSON.stringify(world.datatable); + expect(values).toMatch(value); }); -Then("json should contain {string}", (world, value) => { - const values = [ - ...Object.keys(world.json), - ...(Object.values(world.json) as string[]).map((v) => v.toString()), - ]; - expect(values.includes(value)).toBe(true); +Then("the json should contain {string}", (world, value) => { + const values = JSON.stringify(world.json); + expect(values).toMatch(value); }); When('I set the data variable/value/property {string} to {string}', (world, prop, value) => {