diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6TestRefactorer.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6TestRefactorer.java index e0ad261dc..59a743c17 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6TestRefactorer.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/main/java/com/vaadin/testbench/loadtest/util/K6TestRefactorer.java @@ -499,9 +499,9 @@ private List parseRequestsWithTiming(List lines) { } if (inRequest) { - int opens = countOccurrences(line, '('); + int opens = countUnquotedOccurrences(line, '('); parenCount += opens; - parenCount -= countOccurrences(line, ')'); + parenCount -= countUnquotedOccurrences(line, ')'); if (opens > 0) { seenOpenParen = true; } @@ -647,8 +647,8 @@ private String insertJsessionExtraction(String content) { } if (inFirstAppRequest) { - braceCount += countOccurrences(line, '{'); - braceCount -= countOccurrences(line, '}'); + braceCount += countUnquotedOccurrences(line, '{'); + braceCount -= countUnquotedOccurrences(line, '}'); if (braceCount == 0 && line.contains(")")) { insertLineIndex = i; @@ -687,8 +687,8 @@ private String insertVaadinExtraction(String content) { } if (inInitRequest) { - braceCount += countOccurrences(line, '{'); - braceCount -= countOccurrences(line, '}'); + braceCount += countUnquotedOccurrences(line, '{'); + braceCount -= countUnquotedOccurrences(line, '}'); if (braceCount == 0 && line.contains(")")) { insertLineIndex = i; @@ -705,10 +705,35 @@ private String insertVaadinExtraction(String content) { return content; } - private int countOccurrences(String str, char c) { + /** + * Counts occurrences of {@code target} in {@code str}, ignoring any + * occurrences that fall inside a JavaScript string literal delimited by + * {@code '}, {@code "}, or {@code `}. Backslash escapes inside strings are + * honored. This is what keeps a {@code )} inside a header value like + * {@code 'sec-ch-ua': '"Not/A)Brand";v="99"'} from being mistaken for the + * closing paren of an {@code http.get(...)} call. + *

+ * The scan is per-line: {@link HarToK6Converter} escapes + * {@code \n}/{@code \r}/ {@code \t} in URLs, headers, and bodies, so + * generated string literals never span multiple lines. Template-literal + * interpolations ({@code ${...}}) are treated as opaque string content — + * the converter only emits interpolations of bare identifiers, none of + * which contain {@code (}, {@code )}, {@code {}, or {@code }}. + */ + private int countUnquotedOccurrences(String str, char target) { int count = 0; + char stringDelim = 0; for (int i = 0; i < str.length(); i++) { - if (str.charAt(i) == c) { + char c = str.charAt(i); + if (stringDelim != 0) { + if (c == '\\' && i + 1 < str.length()) { + i++; + } else if (c == stringDelim) { + stringDelim = 0; + } + } else if (c == '\'' || c == '"' || c == '`') { + stringDelim = c; + } else if (c == target) { count++; } } diff --git a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/K6TestRefactorerTest.java b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/K6TestRefactorerTest.java index 82a4edee1..f81ff7a7d 100644 --- a/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/K6TestRefactorerTest.java +++ b/vaadin-testbench-loadtest/testbench-converter-plugin/src/test/java/com/vaadin/testbench/loadtest/util/K6TestRefactorerTest.java @@ -144,6 +144,64 @@ export default function() { + refactored); } + @Test + void refactorContent_thinkTimesEnabled_headerValueContainsCloseParen_sleepIsNotInjectedInsideHeaders() { + K6TestRefactorer refactorer = new K6TestRefactorer( + ThinkTimeConfig.DEFAULT); + + // Reproduces the combineScenarios=true breakage: the Chrome sec-ch-ua + // header value contains a literal `)` inside a single-quoted JS + // string. A non-string-aware paren counter dropped to zero mid-headers + // and the sleep() got injected inside the headers object, producing + // "Unexpected number" parse errors in k6. + String script = """ + import http from 'k6/http' + import { sleep } from 'k6' + export default function() { + // HAR_DELTA_MS: 0 + // Request 1: GET http://localhost:8080/?v-r=init + let response = http.get( + 'http://localhost:8080/?v-r=init', + { + headers: { + 'sec-ch-ua': '"Chromium";v="148", "Not/A)Brand";v="99"', + 'User-Agent': 'Mozilla/5.0' + }, + tags: { name: 'init' } + } + ) + // HAR_DELTA_MS: 200 + // Request 2: POST http://localhost:8080/?v-r=uidl + response = http.post('http://localhost:8080/?v-r=uidl', '{"event":"click"}') + } + """; + + String refactored = refactorer.refactorContent(script); + + // A sleep() call should be emitted (page-read delay at the init + // block boundary). + assertTrue(refactored.contains("sleep("), + "Expected at least one sleep() call in refactored output:\n" + + refactored); + + // The `Not/A)Brand` sec-ch-ua line is the bug trigger. After + // refactoring it must still be immediately followed by the next + // header property — not by a // Think time comment or a sleep() + // call, which would mean the sleep was injected inside the + // headers object. + String triggerLine = "'sec-ch-ua': '\"Chromium\";v=\"148\", \"Not/A)Brand\";v=\"99\"',"; + int triggerIdx = refactored.indexOf(triggerLine); + assertTrue(triggerIdx >= 0, + "Expected sec-ch-ua header line to survive refactoring intact:\n" + + refactored); + String afterTrigger = refactored + .substring(triggerIdx + triggerLine.length()).stripLeading(); + assertTrue(afterTrigger.startsWith("'User-Agent'"), + "sec-ch-ua header was not followed by the User-Agent header — " + + "a sleep() was likely injected inside the headers object:\n" + + refactored); + } + @Test void refactor_readsInputAndWritesOutputFile() throws IOException { Path input = tempDir.resolve("in.js");