UNPKG

k6-cucumber-steps

Version:

Generate k6 test scripts from Cucumber feature files

351 lines (337 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.K6ScriptGenerator = void 0; class K6ScriptGenerator { generateK6File(scenarios, metadata, config) { const header = ` /** * Generated k6 test script */ globalThis.savedTokens = globalThis.savedTokens || {}; globalThis.lastResponse = globalThis.lastResponse || {}; `; const imports = this.generateImports(config, metadata); const options = this.generateOptions(metadata); const testFunction = this.generateTestFunction(scenarios, metadata, config); // ✅ Simplified teardown: just return the tokens (no globalThis needed) const teardownFn = config.language === "ts" ? `export function teardown(tokensFromDefault: Record<string, any>) { return tokensFromDefault; }` : `export function teardown(tokensFromDefault) { return tokensFromDefault; }`; // ✅ handleSummary already uses data.teardown_data — no change needed const handleSummaryFn = config.language === "ts" ? `export function handleSummary(data: any): Record<string, any> { const reports: Record<string, any> = { './reports/summary.html': htmlReport(data), './reports/results.json': JSON.stringify(data), stdout: textSummary(data, { indent: ' ', enableColors: true }), }; console.log('--- Summary File Write Audit ---'); const tokens = data.teardown_data || {}; const keys = Object.keys(tokens).filter(k => k.endsWith('.json')); console.log('Tokens found:', keys.length > 0 ? keys.join(', ') : 'None'); for (const [path, tokenValue] of Object.entries(tokens)) { if (path.endsWith('.json')) { const fullPath = path.startsWith('./') ? path : \`./\${path}\`; reports[fullPath] = JSON.stringify( typeof tokenValue === 'string' ? { access_token: tokenValue } : tokenValue, null, 2 ); console.log(\`✅ Writing: \${fullPath}\`); } } return reports; }` : `export function handleSummary(data) { const reports = { './reports/summary.html': htmlReport(data), './reports/results.json': JSON.stringify(data), stdout: textSummary(data, { indent: ' ', enableColors: true }), }; console.log('--- Summary File Write Audit ---'); const tokens = data["root_group::teardown"] || {}; const keys = Object.keys(tokens).filter(k => k.endsWith('.json')); console.log('Tokens found:', keys.length > 0 ? keys.join(', ') : 'None'); for (const [path, tokenValue] of Object.entries(tokens)) { if (path.endsWith('.json')) { // ✅ Use path directly — no ./ prefix reports[path] = JSON.stringify( typeof tokenValue === 'string' ? { access_token: tokenValue } : tokenValue, null, 2 ); console.log(\`✅ Writing: \${path}\`); } } return reports; }`; return ` ${header} ${imports} ${options} export function setup() { return { v: Date.now() }; } ${testFunction} ${teardownFn} ${handleSummaryFn} `; } generateImports(config, meta) { const ext = config.language === "ts" ? "ts" : "js"; const hasBrowser = meta.some(m => m.tags?.includes("browser")); // ← now used! const baseImports = [ 'import http from "k6/http";', 'import { check, sleep, group } from "k6";', ]; // Add @ts-ignore for remote modules in TS mode if (config.language === "ts") { baseImports.push('// @ts-ignore: k6 resolves remote modules at runtime'); baseImports.push('import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";'); baseImports.push('// @ts-ignore: k6 resolves remote modules at runtime'); baseImports.push('import { textSummary } from "https://jslib.k6.io/k6-summary/0.1.0/index.js";'); } else { baseImports.push('import { htmlReport } from "https://raw.githubusercontent.com/benc-uk/k6-reporter/main/dist/bundle.js";'); baseImports.push('import { textSummary } from "https://jslib.k6.io/k6-summary/0.1.0/index.js";'); } if (hasBrowser) { baseImports.push('import { browser } from "k6/browser";'); } baseImports.push(`import * as steps from "../steps/sample.steps.${ext}";`); return baseImports.join("\n"); } generateOptions(metadata) { let vus = 1; let duration = "30s"; let iterations = null; let stages = []; let thresholds = {}; const hasBrowser = metadata.some((m) => m.tags?.includes("browser")); // 1. Parse Metadata for (const meta of metadata) { if (meta.vus) vus = Math.max(vus, meta.vus); if (meta.duration) duration = meta.duration; if (meta.thresholds) Object.assign(thresholds, meta.thresholds); const iterTag = meta.tags?.find(t => t.startsWith("iterations:")); if (iterTag) { iterations = parseInt(iterTag.split(":")[1]); } if (meta.stages) { stages = meta.stages.split(",").map((s) => { const [d, t] = s.split("-"); return { duration: d.trim(), target: parseInt(t.trim()) }; }); } } // 2. Prepare k6Options object const k6Options = {}; // 3. Add Thresholds (Always allowed at top level) const formattedThresholds = {}; for (const [key, value] of Object.entries(thresholds)) { if (this.isValidK6Metric(key)) { formattedThresholds[key] = [value]; } } if (Object.keys(formattedThresholds).length > 0) { k6Options.thresholds = formattedThresholds; } if (hasBrowser) { // Determine the executor const executor = iterations ? "shared-iterations" : (stages.length > 0 ? "ramping-vus" : "constant-vus"); const scenario = { executor: executor, }; // Apply workload based on executor type if (executor === "shared-iterations") { scenario.vus = vus; scenario.iterations = iterations; } else if (executor === "ramping-vus") { scenario.stages = stages; scenario.startVUs = vus; } else { // constant-vus scenario.vus = vus; scenario.duration = duration; } // Add browser options scenario.options = { browser: { type: "chromium", }, }; k6Options.scenarios = { default: scenario }; } else { // Protocol-based logic if (iterations) { k6Options.scenarios = { default: { executor: "shared-iterations", vus: vus, iterations: iterations, } }; } else { if (stages.length > 0) { k6Options.stages = stages; } else { k6Options.vus = vus; k6Options.duration = duration; } } } return `export const options = ${JSON.stringify(k6Options, null, 2)};`; } isValidK6Metric(metricName) { const validMetrics = [ "http_req_duration", "http_req_failed", "http_req_connecting", "http_req_tls_handshaking", "http_req_waiting", "http_req_receiving", "data_sent", "data_received", "iteration_duration", "iterations", "vus", "vus_max", "browser_web_vital_lcp", "browser_web_vital_fid", "browser_web_vital_cls", ]; return (validMetrics.includes(metricName) || /^[a-z][a-z0-9_]*$/.test(metricName)); } normalizeStepText(text) { // Remove quoted strings first const noQuotes = text.replace(/"[^"]*"|'[^']*'/g, ""); // Normalize "with headers" -> treat as separate keyword let clean = noQuotes .replace(/\s+with\s+headers\s*$/, "") // remove trailing "with headers" .replace(/[^a-zA-Z0-9\s]/g, " ") .trim(); const words = clean.split(/\s+/).filter(w => w); // Check if the step starts with "k6" or contains "k6" as a keyword // If text contains "k6", move it to the front const k6Index = words.findIndex(w => w.toLowerCase() === 'k6'); if (k6Index > 0) { // Remove 'k6' from its position and put it at the front const k6Word = words.splice(k6Index, 1)[0]; words.unshift(k6Word); } return words.map((w, i) => i === 0 ? w.toLowerCase() : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(""); } extractArguments(text) { // Only extract quoted strings (double or single quotes) const matches = text.match(/"([^"]*)"|'([^']*)'/g) || []; // Return the quoted literals as-is (e.g., '"hello"', "'1'") return matches.join(", "); } // Inside K6ScriptGenerator.ts generateTestFunction(scenarios, metadata, config) { const hasAnyBrowser = metadata.some((m) => m.tags?.includes("browser")); const lines = []; // Open browser if any scenario uses it if (hasAnyBrowser) { lines.push(`let page;`); lines.push(`try {`); lines.push(` page = await browser.newPage();`); lines.push(` console.log('Browser page opened once for all scenarios');`); // Set fallback base URL only if needed lines.push(` steps.k6TheBaseUrlIs("https://demoqa.com");`); lines.push(` console.log('Fallback baseUrl set to:', globalThis.baseUrl || 'not set');`); lines.push(``); } // Process ALL scenarios (both browser and HTTP) scenarios.forEach((scenario) => { const isBrowser = scenario.tags?.includes("browser"); // Init group lines.push(` group(${JSON.stringify(scenario.name + " - init")}, () => {`); lines.push(` console.log('Initiating scenario: ${scenario.name}');`); lines.push(` });`); if (isBrowser) { lines.push(` try {`); scenario.steps.forEach((step) => { const name = this.normalizeStepText(step.text); const argsStr = this.extractArguments(step.text); const args = argsStr ? argsStr.split(/,\s*/).filter(Boolean) : []; const needsPage = [ "navigate", "click", "see", "fill", "type", "press", "waitfor", "wait", "locator", "select", "title", "url", "element", "shouldsee", "shouldnotsee", "button", "find" ].some(kw => name.toLowerCase().includes(kw)) && !["thebaseurlis", "thebaseurl"].some(kw => name.toLowerCase().includes(kw)); const params = []; if (needsPage) { params.push("page"); } params.push(...args); if (step.argument && typeof step.argument === 'string') { params.push(JSON.stringify(step.argument)); } const callPrefix = needsPage || params.length > 1 ? 'await ' : ''; lines.push(` ${callPrefix}steps.${name}(${params.join(", ")});`); }); lines.push(` } catch (err) {`); const stackAccessCode = config.language === "ts" ? "(err as any)?.stack || 'No stack'" : "err?.stack || 'No stack'"; lines.push(` console.error('Error in ${scenario.name}:', err);`); lines.push(` console.error('Stack:', ${stackAccessCode});`); lines.push(` }`); } else { // Non-browser (HTTP/API) steps scenario.steps.forEach((step) => { const name = this.normalizeStepText(step.text); const argsStr = this.extractArguments(step.text); const args = argsStr ? argsStr.split(/,\s*/).filter(Boolean) : []; const params = []; params.push(...args); if (step.argument && typeof step.argument === 'string') { params.push(JSON.stringify(step.argument)); } if (step.argument && Array.isArray(step.argument)) { params.push(JSON.stringify(step.argument)); } lines.push(` steps.${name}(${params.join(", ")});`); }); } // Cleanup group lines.push(` group(${JSON.stringify(scenario.name + " - cleanup")}, () => {`); lines.push(` console.log('Finished: ${scenario.name}');`); lines.push(` });`); lines.push(` sleep(1);`); lines.push(``); }); // Close browser if opened if (hasAnyBrowser) { lines.push(`} finally {`); lines.push(` if (page) {`); lines.push(` try { await page.close(); await sleep(0.5); } catch (e) { console.warn('Final page close failed:', e); }`); lines.push(` }`); lines.push(`}`); } return ` export default async function () { ${lines.join("\n")} console.log('🔍 Final savedTokens:', JSON.stringify(globalThis.savedTokens)); return globalThis.savedTokens || {}; }`; } } exports.K6ScriptGenerator = K6ScriptGenerator; //# sourceMappingURL=k6-script.generator.js.map