From ef66d87a5c7097e73eecca17f510f47076d8bd34 Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:15:26 -0800 Subject: [PATCH 1/7] Add support for more variations of CommonMark syntax --- dev/doc-content.md | 30 ++- dev/index.js | 2 +- src/config.js | 31 +++ src/index.test.js | 526 +++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 17 +- 5 files changed, 592 insertions(+), 14 deletions(-) diff --git a/dev/doc-content.md b/dev/doc-content.md index 1cc3d3a..21f2d0c 100644 --- a/dev/doc-content.md +++ b/dev/doc-content.md @@ -1,11 +1,19 @@ -``` -PATCH https://orc.wiremockapi.cloud/satellites/8 HTTP/1.1 -Host: orc.wiremockapi.cloud -Content-Type: application/json -Accept: application/json -Content-Length: 25 - -{ - "status": "maintenance" -} -``` \ No newline at end of file +[comment]: # 'test {"testId": "uppercase-conversion", "detectSteps": false}' + +1. Open the app at [http://localhost:3000](http://localhost:3000). + +[comment]: # 'step {"goTo": "http://localhost:3000"}' + +2. Type "hello world" in the input field. + +[comment]: # 'step {"find": {"selector": "#input", "click": true}}' +[comment]: # 'step {"type": "hello world"}' + +3. Click **Convert to Uppercase**. + +[comment]: # 'step {"find": {"selector": "button", "click": true}}' + +4. You'll see **HELLO WORLD** in the output. + +[comment]: # 'step {"find": "HELLO WORLD"}' +[comment]: # "test end" diff --git a/dev/index.js b/dev/index.js index 847624b..904d685 100644 --- a/dev/index.js +++ b/dev/index.js @@ -12,7 +12,7 @@ main(); */ async function main() { const json = { - input: "dev/doc-content.dita", + input: "dev/doc-content.md", logLevel: "debug", runOn: [ { diff --git a/src/config.js b/src/config.js index 72eb211..3941c21 100644 --- a/src/config.js +++ b/src/config.js @@ -320,26 +320,57 @@ let defaultFileTypes = { testStart: [ "{\\/\\*\\s*test\\s+?([\\s\\S]*?)\\s*\\*\\/}", "", + // CommonMark comment syntax with parentheses: [comment]: # (test ...) "\\[comment\\]:\\s+#\\s+\\(test\\s*(.*?)\\s*\\)", "\\[comment\\]:\\s+#\\s+\\(test start\\s*(.*?)\\s*\\)", + // CommonMark comment syntax with single quotes: [comment]: # 'test ...' + "\\[comment\\]:\\s+#\\s+'test\\s*(.*?)\\s*'", + "\\[comment\\]:\\s+#\\s+'test start\\s*(.*?)\\s*'", + // CommonMark comment syntax with double quotes: [comment]: # "test ..." + // Uses (?:[^"\\]|\\.)* to handle escaped quotes within the content + '\\[comment\\]:\\s+#\\s+"test\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', + '\\[comment\\]:\\s+#\\s+"test start\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', ], testEnd: [ "{\\/\\*\\s*test end\\s*\\*\\/}", "", + // CommonMark comment syntax with parentheses "\\[comment\\]:\\s+#\\s+\\(test end\\)", + // CommonMark comment syntax with single quotes + "\\[comment\\]:\\s+#\\s+'test end'", + // CommonMark comment syntax with double quotes + '\\[comment\\]:\\s+#\\s+"test end"', ], ignoreStart: [ "{\\/\\*\\s*test ignore start\\s*\\*\\/}", "", + // CommonMark comment syntax with parentheses + "\\[comment\\]:\\s+#\\s+\\(test ignore start\\)", + // CommonMark comment syntax with single quotes + "\\[comment\\]:\\s+#\\s+'test ignore start'", + // CommonMark comment syntax with double quotes + '\\[comment\\]:\\s+#\\s+"test ignore start"', ], ignoreEnd: [ "{\\/\\*\\s*test ignore end\\s*\\*\\/}", "", + // CommonMark comment syntax with parentheses + "\\[comment\\]:\\s+#\\s+\\(test ignore end\\)", + // CommonMark comment syntax with single quotes + "\\[comment\\]:\\s+#\\s+'test ignore end'", + // CommonMark comment syntax with double quotes + '\\[comment\\]:\\s+#\\s+"test ignore end"', ], step: [ "{\\/\\*\\s*step\\s+?([\\s\\S]*?)\\s*\\*\\/}", "", + // CommonMark comment syntax with parentheses: [comment]: # (step ...) "\\[comment\\]:\\s+#\\s+\\(step\\s*(.*?)\\s*\\)", + // CommonMark comment syntax with single quotes: [comment]: # 'step ...' + "\\[comment\\]:\\s+#\\s+'step\\s*(.*?)\\s*'", + // CommonMark comment syntax with double quotes: [comment]: # "step ..." + // Uses (?:[^"\\]|\\.)* to handle escaped quotes within the content + '\\[comment\\]:\\s+#\\s+"step\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', ], }, markup: [ diff --git a/src/index.test.js b/src/index.test.js index de8c1c1..0e8ed35 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -885,3 +885,529 @@ detectSteps: true expect(codeblockStep).to.exist; }); }); + +// CommonMark comment syntax test inputs - JSON syntax +const markdownParenthesesComments = ` +[comment]: # (test {"testId": "parentheses-test", "detectSteps": false}) + +1. Open the app at [http://localhost:3000](http://localhost:3000). + +[comment]: # (step {"goTo": "http://localhost:3000"}) + +2. Type "hello world" in the input field. + +[comment]: # (step {"find": {"selector": "#input", "click": true}}) +[comment]: # (step {"type": "hello world"}) + +3. Click **Convert to Uppercase**. + +[comment]: # (step {"find": {"selector": "button", "click": true}}) + +4. You'll see **HELLO WORLD** in the output. + +[comment]: # (step {"find": "HELLO WORLD"}) +[comment]: # (test end) +`; + +const markdownSingleQuoteComments = ` +[comment]: # 'test {"testId": "single-quote-test", "detectSteps": false}' + +1. Open the app at [http://localhost:3000](http://localhost:3000). + +[comment]: # 'step {"goTo": "http://localhost:3000"}' + +2. Type "hello world" in the input field. + +[comment]: # 'step {"find": {"selector": "#input", "click": true}}' +[comment]: # 'step {"type": "hello world"}' + +3. Click **Convert to Uppercase**. + +[comment]: # 'step {"find": {"selector": "button", "click": true}}' + +4. You'll see **HELLO WORLD** in the output. + +[comment]: # 'step {"find": "HELLO WORLD"}' +[comment]: # 'test end' +`; + +const markdownDoubleQuoteComments = ` +[comment]: # "test {\\"testId\\": \\"double-quote-test\\", \\"detectSteps\\": false}" + +1. Open the app at [http://localhost:3000](http://localhost:3000). + +[comment]: # "step {\\"goTo\\": \\"http://localhost:3000\\"}" + +2. Type "hello world" in the input field. + +[comment]: # "step {\\"find\\": {\\"selector\\": \\"#input\\", \\"click\\": true}}" +[comment]: # "step {\\"type\\": \\"hello world\\"}" + +3. Click **Convert to Uppercase**. + +[comment]: # "step {\\"find\\": {\\"selector\\": \\"button\\", \\"click\\": true}}" + +4. You'll see **HELLO WORLD** in the output. + +[comment]: # "step {\\"find\\": \\"HELLO WORLD\\"}" +[comment]: # "test end" +`; + +const markdownMixedQuoteComments = ` +[comment]: # (test {"testId": "mixed-quote-test", "detectSteps": false}) + +1. Open the app at [http://localhost:3000](http://localhost:3000). + +[comment]: # 'step {"goTo": "http://localhost:3000"}' + +2. Type "hello world" in the input field. + +[comment]: # "step {\\"find\\": {\\"selector\\": \\"#input\\", \\"click\\": true}}" +[comment]: # (step {"type": "hello world"}) + +3. Click **Convert to Uppercase**. + +[comment]: # 'step {"find": {"selector": "button", "click": true}}' + +4. You'll see **HELLO WORLD** in the output. + +[comment]: # (step {"find": "HELLO WORLD"}) +[comment]: # "test end" +`; + +const markdownIgnoreSyntax = ` +[comment]: # (test {"testId": "ignore-syntax-test", "detectSteps": true}) + +This text should be detected. + +**Visible text** + +[comment]: # 'test ignore start' + +**Ignored text that should not be detected** + +[comment]: # 'test ignore end' + +**More visible text** + +[comment]: # "test end" +`; + +describe("CommonMark Comment Syntax Tests", function () { + it("should correctly parse markdown with parentheses syntax: [comment]: # (test ...)", async function () { + const tempFile = "temp_parentheses.md"; + fs.writeFileSync(tempFile, markdownParenthesesComments.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("parentheses-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(5); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("find").that.deep.includes({ selector: "#input", click: true }); + expect(steps[2]).to.have.property("type").that.equals("hello world"); + expect(steps[3]).to.have.property("find").that.deep.includes({ selector: "button", click: true }); + expect(steps[4]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse markdown with single quote syntax: [comment]: # 'test ...'", async function () { + const tempFile = "temp_single_quote.md"; + fs.writeFileSync(tempFile, markdownSingleQuoteComments.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("single-quote-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(5); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("find").that.deep.includes({ selector: "#input", click: true }); + expect(steps[2]).to.have.property("type").that.equals("hello world"); + expect(steps[3]).to.have.property("find").that.deep.includes({ selector: "button", click: true }); + expect(steps[4]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse markdown with double quote syntax: [comment]: # \"test ...\"", async function () { + const tempFile = "temp_double_quote.md"; + fs.writeFileSync(tempFile, markdownDoubleQuoteComments.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("double-quote-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(5); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("find").that.deep.includes({ selector: "#input", click: true }); + expect(steps[2]).to.have.property("type").that.equals("hello world"); + expect(steps[3]).to.have.property("find").that.deep.includes({ selector: "button", click: true }); + expect(steps[4]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse markdown with mixed quote syntaxes in same file", async function () { + const tempFile = "temp_mixed_quote.md"; + fs.writeFileSync(tempFile, markdownMixedQuoteComments.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("mixed-quote-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(5); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("find").that.deep.includes({ selector: "#input", click: true }); + expect(steps[2]).to.have.property("type").that.equals("hello world"); + expect(steps[3]).to.have.property("find").that.deep.includes({ selector: "button", click: true }); + expect(steps[4]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly handle ignore start/end with different quote syntaxes", async function () { + const tempFile = "temp_ignore_syntax.md"; + fs.writeFileSync(tempFile, markdownIgnoreSyntax.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("ignore-syntax-test"); + + // NOTE: The ignore functionality is currently not implemented (the ignore variable + // is set but not used to filter detected steps). This test validates that the + // ignore start/end patterns with different quote syntaxes are at least recognized. + // When ignore filtering is implemented, update this test to verify ignored content + // is excluded from detected steps. + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array"); + // Currently all bold text is detected (ignore not implemented) + expect(steps.length).to.be.greaterThan(0); + }); +}); + +// CommonMark comment syntax with YAML content +const markdownParenthesesYaml = ` +[comment]: # (test testId: parentheses-yaml-test) + +1. Open the app. + +[comment]: # (step goTo: "http://localhost:3000") + +2. Type in the field. + +[comment]: # (step type: hello world) + +3. You'll see the output. + +[comment]: # (step find: HELLO WORLD) +[comment]: # (test end) +`; + +const markdownSingleQuoteYaml = ` +[comment]: # 'test testId: single-quote-yaml-test' + +1. Open the app. + +[comment]: # 'step goTo: "http://localhost:3000"' + +2. Type in the field. + +[comment]: # 'step type: hello world' + +3. You'll see the output. + +[comment]: # 'step find: HELLO WORLD' +[comment]: # 'test end' +`; + +const markdownDoubleQuoteYaml = ` +[comment]: # "test testId: double-quote-yaml-test" + +1. Open the app. + +[comment]: # "step goTo: http://localhost:3000" + +2. Type in the field. + +[comment]: # "step type: hello world" + +3. You'll see the output. + +[comment]: # "step find: HELLO WORLD" +[comment]: # "test end" +`; + +// CommonMark comment syntax with XML attribute content +const markdownParenthesesXml = ` +[comment]: # (test testId="parentheses-xml-test" detectSteps=false) + +1. Open the app. + +[comment]: # (step goTo="http://localhost:3000") + +2. Type in the field. + +[comment]: # (step type="hello world") + +3. Wait for result. + +[comment]: # (step wait=500) + +4. You'll see the output. + +[comment]: # (step find="HELLO WORLD") +[comment]: # (test end) +`; + +const markdownSingleQuoteXml = ` +[comment]: # 'test testId="single-quote-xml-test" detectSteps=false' + +1. Open the app. + +[comment]: # 'step goTo="http://localhost:3000"' + +2. Type in the field. + +[comment]: # 'step type="hello world"' + +3. Wait for result. + +[comment]: # 'step wait=500' + +4. You'll see the output. + +[comment]: # 'step find="HELLO WORLD"' +[comment]: # 'test end' +`; + +const markdownDoubleQuoteXml = ` +[comment]: # "test testId='double-quote-xml-test' detectSteps=false" + +1. Open the app. + +[comment]: # "step goTo='http://localhost:3000'" + +2. Type in the field. + +[comment]: # "step type='hello world'" + +3. Wait for result. + +[comment]: # "step wait=500" + +4. You'll see the output. + +[comment]: # "step find='HELLO WORLD'" +[comment]: # "test end" +`; + +// CommonMark with XML dot notation +const markdownParenthesesXmlDotNotation = ` +[comment]: # (test testId="parentheses-xml-dot-test" detectSteps=false) + +1. Make an API call. + +[comment]: # (step httpRequest.url="https://example.com/api" httpRequest.method="GET") + +2. Another call. + +[comment]: # (step httpRequest.url="https://example.com/submit" httpRequest.method="POST" httpRequest.request.body="test") +[comment]: # (test end) +`; + +const markdownSingleQuoteXmlDotNotation = ` +[comment]: # 'test testId="single-quote-xml-dot-test" detectSteps=false' + +1. Make an API call. + +[comment]: # 'step httpRequest.url="https://example.com/api" httpRequest.method="GET"' + +2. Another call. + +[comment]: # 'step httpRequest.url="https://example.com/submit" httpRequest.method="POST" httpRequest.request.body="test"' +[comment]: # 'test end' +`; + +describe("CommonMark Comment Syntax with YAML Tests", function () { + it("should correctly parse parentheses syntax with YAML content: [comment]: # (test key: value)", async function () { + const tempFile = "temp_paren_yaml.md"; + fs.writeFileSync(tempFile, markdownParenthesesYaml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("parentheses-yaml-test"); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(3); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse single quote syntax with YAML content: [comment]: # 'test key: value'", async function () { + const tempFile = "temp_single_yaml.md"; + fs.writeFileSync(tempFile, markdownSingleQuoteYaml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("single-quote-yaml-test"); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(3); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse double quote syntax with YAML content: [comment]: # \"test key: value\"", async function () { + const tempFile = "temp_double_yaml.md"; + fs.writeFileSync(tempFile, markdownDoubleQuoteYaml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("double-quote-yaml-test"); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(3); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("find").that.equals("HELLO WORLD"); + }); +}); + +describe("CommonMark Comment Syntax with XML Attribute Tests", function () { + it("should correctly parse parentheses syntax with XML attributes: [comment]: # (test key=\"value\")", async function () { + const tempFile = "temp_paren_xml.md"; + fs.writeFileSync(tempFile, markdownParenthesesXml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("parentheses-xml-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(4); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("wait").that.equals(500); + expect(steps[3]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse single quote syntax with XML attributes: [comment]: # 'test key=\"value\"'", async function () { + const tempFile = "temp_single_xml.md"; + fs.writeFileSync(tempFile, markdownSingleQuoteXml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("single-quote-xml-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(4); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("wait").that.equals(500); + expect(steps[3]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse double quote syntax with XML attributes using single quotes inside: [comment]: # \"test key='value'\"", async function () { + const tempFile = "temp_double_xml.md"; + fs.writeFileSync(tempFile, markdownDoubleQuoteXml.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("double-quote-xml-test"); + expect(results.specs[0].tests[0].detectSteps).to.equal(false); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(4); + expect(steps[0]).to.have.property("goTo").that.equals("http://localhost:3000"); + expect(steps[1]).to.have.property("type").that.equals("hello world"); + expect(steps[2]).to.have.property("wait").that.equals(500); + expect(steps[3]).to.have.property("find").that.equals("HELLO WORLD"); + }); + + it("should correctly parse parentheses syntax with XML dot notation: [comment]: # (step key.nested=\"value\")", async function () { + const tempFile = "temp_paren_xml_dot.md"; + fs.writeFileSync(tempFile, markdownParenthesesXmlDotNotation.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("parentheses-xml-dot-test"); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(2); + + expect(steps[0]).to.have.property("httpRequest"); + expect(steps[0].httpRequest).to.have.property("url").that.equals("https://example.com/api"); + expect(steps[0].httpRequest).to.have.property("method").that.equals("GET"); + + expect(steps[1]).to.have.property("httpRequest"); + expect(steps[1].httpRequest).to.have.property("url").that.equals("https://example.com/submit"); + expect(steps[1].httpRequest).to.have.property("method").that.equals("POST"); + expect(steps[1].httpRequest).to.have.property("request"); + expect(steps[1].httpRequest.request).to.have.property("body").that.equals("test"); + }); + + it("should correctly parse single quote syntax with XML dot notation: [comment]: # 'step key.nested=\"value\"'", async function () { + const tempFile = "temp_single_xml_dot.md"; + fs.writeFileSync(tempFile, markdownSingleQuoteXmlDotNotation.trim()); + const config = { input: tempFile }; + const results = await detectAndResolveTests({ config }); + fs.unlinkSync(tempFile); + + expect(results.specs).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests).to.be.an("array").that.has.lengthOf(1); + expect(results.specs[0].tests[0].testId).to.equal("single-quote-xml-dot-test"); + + const steps = results.specs[0].tests[0].contexts[0].steps; + expect(steps).to.be.an("array").that.has.lengthOf(2); + + expect(steps[0]).to.have.property("httpRequest"); + expect(steps[0].httpRequest).to.have.property("url").that.equals("https://example.com/api"); + expect(steps[0].httpRequest).to.have.property("method").that.equals("GET"); + + expect(steps[1]).to.have.property("httpRequest"); + expect(steps[1].httpRequest).to.have.property("url").that.equals("https://example.com/submit"); + expect(steps[1].httpRequest).to.have.property("method").that.equals("POST"); + expect(steps[1].httpRequest.request).to.have.property("body").that.equals("test"); + }); +}); + diff --git a/src/utils.js b/src/utils.js index 955b232..ed1fc3a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -130,13 +130,26 @@ function parseObject({ stringifiedObject }) { return xmlAttrs; } + const trimmedString = stringifiedObject.trim(); + + // Check if this looks like escaped JSON (starts with { or [ and contains \") + // This handles cases like: [comment]: # "test {\"key\": \"value\"}" + // Only unescape when we have JSON-like content with escaped quotes + const looksLikeEscapedJson = + (trimmedString.startsWith("{") || trimmedString.startsWith("[")) && + trimmedString.includes('\\"'); + + const stringToParse = looksLikeEscapedJson + ? stringifiedObject.replace(/\\"/g, '"') + : stringifiedObject; + // If string, try to parse as JSON or YAML try { - const json = JSON.parse(stringifiedObject); + const json = JSON.parse(stringToParse); return json; } catch (jsonError) { try { - const yaml = YAML.parse(stringifiedObject); + const yaml = YAML.parse(stringToParse); return yaml; } catch (yamlError) { throw new Error("Invalid JSON or YAML format"); From 1d8228edda4373565d9f460f897c620b1bfcdb57 Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:20:51 -0800 Subject: [PATCH 2/7] Normalize test-level detectSteps --- src/utils.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index ed1fc3a..480f47a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -611,8 +611,14 @@ async function parseContent({ config, content, filePath, fileType }) { // If the testId doesn't exist, set it test.testId = `${testId}`; } + // Normalize detectSteps field + if (test.detectSteps === "false") { + test.detectSteps = false; + } else if (test.detectSteps === "true") { + test.detectSteps = true; + } + // If the test doesn't have steps, add an empty array if (!test.steps) { - // If the test doesn't have steps, add an empty array test.steps = []; } tests.push(test); From 0126a229a8d89e62e9c96c7ef9bc48e4b0555abe Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:28:51 -0800 Subject: [PATCH 3/7] Improve double-quoted JSON parsing --- src/utils.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/utils.js b/src/utils.js index 480f47a..650c1ec 100644 --- a/src/utils.js +++ b/src/utils.js @@ -130,19 +130,23 @@ function parseObject({ stringifiedObject }) { return xmlAttrs; } - const trimmedString = stringifiedObject.trim(); + let stringToParse = stringifiedObject; - // Check if this looks like escaped JSON (starts with { or [ and contains \") - // This handles cases like: [comment]: # "test {\"key\": \"value\"}" - // Only unescape when we have JSON-like content with escaped quotes + // Check if this looks like escaped JSON + const trimmedString = stringifiedObject.trim(); const looksLikeEscapedJson = (trimmedString.startsWith("{") || trimmedString.startsWith("[")) && trimmedString.includes('\\"'); - const stringToParse = looksLikeEscapedJson - ? stringifiedObject.replace(/\\"/g, '"') - : stringifiedObject; - + if (looksLikeEscapedJson) { + try { + // Attempt to parse as double-encoded JSON first + stringToParse = JSON.parse('"' + stringifiedObject + '"'); + } catch { + // Fallback to simple quote replacement for basic cases + stringToParse = stringifiedObject.replace(/\\"/g, '"'); + } + } // If string, try to parse as JSON or YAML try { const json = JSON.parse(stringToParse); From 9efdf86e14ebf47c73d494fb4fd837f8a981ab6f Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:44:16 -0800 Subject: [PATCH 4/7] Fix log level --- src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.js b/src/config.js index 3941c21..6981def 100644 --- a/src/config.js +++ b/src/config.js @@ -503,7 +503,7 @@ async function setConfig({ config }) { } catch (error) { log( config, - "warn", + "warning", `Invalid JSON in DOC_DETECTIVE environment variable: ${error.message}. Ignoring config overrides.` ); } From 158bab0bfab8d6d78e02ae959b0028b79e502f41 Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:50:44 -0800 Subject: [PATCH 5/7] Enhance JSON and YAML parsing logic to handle escaped/double-encoded JSON --- src/utils.js | 47 +++++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/utils.js b/src/utils.js index 650c1ec..8cbdfbe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -130,30 +130,37 @@ function parseObject({ stringifiedObject }) { return xmlAttrs; } - let stringToParse = stringifiedObject; - - // Check if this looks like escaped JSON - const trimmedString = stringifiedObject.trim(); - const looksLikeEscapedJson = - (trimmedString.startsWith("{") || trimmedString.startsWith("[")) && - trimmedString.includes('\\"'); - - if (looksLikeEscapedJson) { - try { - // Attempt to parse as double-encoded JSON first - stringToParse = JSON.parse('"' + stringifiedObject + '"'); - } catch { - // Fallback to simple quote replacement for basic cases - stringToParse = stringifiedObject.replace(/\\"/g, '"'); - } - } - // If string, try to parse as JSON or YAML + // Try to parse as JSON first (handles valid JSON including those with escaped quotes in string values) try { - const json = JSON.parse(stringToParse); + const json = JSON.parse(stringifiedObject); return json; } catch (jsonError) { + // JSON parsing failed - check if this looks like escaped/double-encoded JSON + const trimmedString = stringifiedObject.trim(); + const looksLikeEscapedJson = + (trimmedString.startsWith("{") || trimmedString.startsWith("[")) && + trimmedString.includes('\\"'); + + if (looksLikeEscapedJson) { + let stringToParse = stringifiedObject; + try { + // Attempt to parse as double-encoded JSON + stringToParse = JSON.parse('"' + stringifiedObject + '"'); + } catch { + // Fallback to simple quote replacement for basic cases + stringToParse = stringifiedObject.replace(/\\"/g, '"'); + } + try { + const json = JSON.parse(stringToParse); + return json; + } catch { + // Fall through to YAML parsing + } + } + + // Try YAML as final fallback try { - const yaml = YAML.parse(stringToParse); + const yaml = YAML.parse(stringifiedObject); return yaml; } catch (yamlError) { throw new Error("Invalid JSON or YAML format"); From f895330b4ffd24a6098f62ecc7973ac043fed88c Mon Sep 17 00:00:00 2001 From: hawkeyexl Date: Wed, 3 Dec 2025 10:53:19 -0800 Subject: [PATCH 6/7] Fix warning-checking test --- src/config.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.test.js b/src/config.test.js index 3880c5b..decedb1 100644 --- a/src/config.test.js +++ b/src/config.test.js @@ -81,7 +81,7 @@ describe("envMerge", function () { // Should continue normally without applying overrides expect(validStub.calledOnce).to.be.true; - expect(logStub.calledWith(sinon.match.any, "warn", sinon.match.string)).to.be.true; + expect(logStub.calledWith(sinon.match.any, "warning", sinon.match.string)).to.be.true; }); it("should handle DOC_DETECTIVE environment variable without config property", async function () { From e258e2137fb38159dc3dac560c23b29af97391c1 Mon Sep 17 00:00:00 2001 From: Manny Silva Date: Wed, 3 Dec 2025 11:03:52 -0800 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/config.js | 4 ++-- src/utils.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.js b/src/config.js index 6981def..8bacb3a 100644 --- a/src/config.js +++ b/src/config.js @@ -327,7 +327,7 @@ let defaultFileTypes = { "\\[comment\\]:\\s+#\\s+'test\\s*(.*?)\\s*'", "\\[comment\\]:\\s+#\\s+'test start\\s*(.*?)\\s*'", // CommonMark comment syntax with double quotes: [comment]: # "test ..." - // Uses (?:[^"\\]|\\.)* to handle escaped quotes within the content + // Uses (?:[^"\\\\]|\\\\.)* to handle escaped quotes within the content '\\[comment\\]:\\s+#\\s+"test\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', '\\[comment\\]:\\s+#\\s+"test start\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', ], @@ -369,7 +369,7 @@ let defaultFileTypes = { // CommonMark comment syntax with single quotes: [comment]: # 'step ...' "\\[comment\\]:\\s+#\\s+'step\\s*(.*?)\\s*'", // CommonMark comment syntax with double quotes: [comment]: # "step ..." - // Uses (?:[^"\\]|\\.)* to handle escaped quotes within the content + // Uses (?:[^"\\\\]|\\\\.)* to handle escaped quotes within the content '\\[comment\\]:\\s+#\\s+"step\\s*((?:[^"\\\\]|\\\\.)*)\\s*"', ], }, diff --git a/src/utils.js b/src/utils.js index 8cbdfbe..481d609 100644 --- a/src/utils.js +++ b/src/utils.js @@ -142,7 +142,7 @@ function parseObject({ stringifiedObject }) { trimmedString.includes('\\"'); if (looksLikeEscapedJson) { - let stringToParse = stringifiedObject; + let stringToParse; try { // Attempt to parse as double-encoded JSON stringToParse = JSON.parse('"' + stringifiedObject + '"');