UNPKG

@labnex/cli

Version:

CLI for Labnex, an AI-Powered Testing Automation Platform

766 lines 45.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TestStepParser = void 0; const parserHelpers_1 = require("./lib/parserHelpers"); /** Utility: always return a concrete string (never undefined). */ const str = (maybe) => maybe ?? ''; class TestStepParser { static _tryParseWaitAction(normalizedStep, originalStep) { const timePattern = /^(?:wait|pause)(?:\s+for)?\s+(\d+)\s*(milliseconds?|ms|seconds?|s)?/i; const timeMatch = normalizedStep.match(timePattern); const selectorPattern = /^(?:wait|pause)\s+for\s+(.+)/i; const selectorMatch = normalizedStep.match(selectorPattern); if (timeMatch && timeMatch[1]) { const timeoutValue = parseInt(timeMatch[1], 10); const unit = timeMatch[2]?.toLowerCase(); let timeoutMs = timeoutValue; if (unit === 's' || unit === 'seconds' || !unit) { timeoutMs = timeoutValue * 1000; } (0, parserHelpers_1.addLog)(`[WaitBlockEntered] Parsed timeout: ${timeoutMs}ms. Returning 'wait' action.`); return { action: 'wait', timeout: timeoutMs, originalStep: originalStep }; } if (selectorMatch && selectorMatch[1]) { const selectorText = selectorMatch[1].trim(); (0, parserHelpers_1.addLog)(`[WaitSelector] Parsed selector wait for: ${selectorText}`); return { action: 'wait', target: selectorText, originalStep: originalStep }; } (0, parserHelpers_1.addLog)(`[WaitBlockSkipped] No wait pattern matched.`); return null; } static _tryParseIframeSwitch(currentStep, normalizedCurrentStep, originalStepInput) { (0, parserHelpers_1.addLog)(`[DEBUG_IFRAME_SWITCH] Entered _tryParseIframeSwitch. currentStep: "${currentStep}", normalized: "${normalizedCurrentStep}"`); const switchToIframePattern = /(?:switch to|enter|focus on) iframe\s+(.+)/i; const iframeMatch = currentStep.match(switchToIframePattern); (0, parserHelpers_1.addLog)(`[DEBUG_IFRAME_SWITCH] iframeMatch result: ${JSON.stringify(iframeMatch)}`); if (iframeMatch && iframeMatch[1]) { const rawArg = iframeMatch[1].trim(); (0, parserHelpers_1.addLog)(`[DEBUG_IFRAME_SWITCH] rawArg from iframeMatch[1]: "${rawArg}"`); let finalIframeSelector; let hintParseResult = (0, parserHelpers_1.extractHintedSelector)(rawArg); if (hintParseResult.selectorValue) { finalIframeSelector = (0, parserHelpers_1.finalizeSelector)(str(hintParseResult.type), str(hintParseResult.selectorValue)); (0, parserHelpers_1.addLog)(`[switchToIframe] Parsed as direct hint. Type: ${str(hintParseResult.type)}, Final Selector: "${finalIframeSelector}"`); } else { // Maybe it was quoted; try extracting the quoted text first const unquotedArg = (0, parserHelpers_1.extractQuotedText)(rawArg); if (unquotedArg !== undefined) { (0, parserHelpers_1.addLog)(`[switchToIframe] Raw argument not a direct hint. Unquoted to: "${unquotedArg}"`); hintParseResult = (0, parserHelpers_1.extractHintedSelector)(unquotedArg); if (hintParseResult.selectorValue) { finalIframeSelector = (0, parserHelpers_1.finalizeSelector)(str(hintParseResult.type), str(hintParseResult.selectorValue)); (0, parserHelpers_1.addLog)(`[switchToIframe] Parsed unquoted as hint. Type: ${str(hintParseResult.type)}, Final Selector: "${finalIframeSelector}"`); } else { // Use the unquoted text literally finalIframeSelector = unquotedArg; (0, parserHelpers_1.addLog)(`[switchToIframe] Unquoted not a hint. Using as raw selector: "${finalIframeSelector}"`); } } else { // Neither a hint nor a quoted string—use rawArg verbatim finalIframeSelector = rawArg; (0, parserHelpers_1.addLog)(`[switchToIframe] Not a hint, not quoted. Using as raw selector: "${finalIframeSelector}"`); } } (0, parserHelpers_1.addLog)(`[ParseSuccess] Action: switchToIframe, Target: ${finalIframeSelector}`); return { action: 'switchToIframe', // finalIframeSelector is guaranteed to be a string here, so the "!" is safe: target: finalIframeSelector, originalStep: originalStepInput }; } const switchToMainContentPattern = /(?:switch to|return to|exit to|focus on) (?:main|default|top|parent) (?:content|document|page|frame)/i; if (switchToMainContentPattern.test(normalizedCurrentStep)) { (0, parserHelpers_1.addLog)(`[ParseSuccess] Action: switchToMainContent`); return { action: 'switchToMainContent', originalStep: originalStepInput }; } return null; } static _tryParseUploadAction(currentStep, originalStepInput) { // Pattern: "upload <filePath> to [element] <selector>" const uploadPattern = /(?:upload|attach file|set file)\s+(.+?)\s+(?:to|on|into)(?:\s+element)?\s+(.+)/i; const uploadMatch = currentStep.match(uploadPattern); if (uploadMatch && uploadMatch[1] && uploadMatch[2]) { let filePathValue = (0, parserHelpers_1.extractQuotedText)(uploadMatch[1].trim()) || uploadMatch[1].trim(); if (filePathValue === '""' || filePathValue === "''") { filePathValue = ""; // handle an empty quoted string } let targetDescription = uploadMatch[2].trim(); (0, parserHelpers_1.addLog)(`[UploadParser] Initial targetDescription: "${targetDescription}"`); // If the target was quoted, extract it now: const unquotedTargetDesc = (0, parserHelpers_1.extractQuotedText)(targetDescription); if (unquotedTargetDesc !== undefined) { (0, parserHelpers_1.addLog)(`[UploadParser] Unquoted targetDescription: "${unquotedTargetDesc}"`); targetDescription = unquotedTargetDesc; } let finalUploadSelector; const hintResult = (0, parserHelpers_1.extractHintedSelector)(targetDescription); (0, parserHelpers_1.addLog)(`[UploadParser] Hint result for "${targetDescription}": Type=${str(hintResult.type)}, Value=${str(hintResult.selectorValue)}`); if (hintResult.type && hintResult.selectorValue) { finalUploadSelector = (0, parserHelpers_1.finalizeSelector)(str(hintResult.type), str(hintResult.selectorValue)); (0, parserHelpers_1.addLog)(`[UploadParser] Final selector from hint: "${finalUploadSelector}"`); } else { // No valid hint—just use targetDescription literally finalUploadSelector = targetDescription; (0, parserHelpers_1.addLog)(`[UploadParser] No valid hint, using targetDescription as selector: "${finalUploadSelector}"`); } if (!finalUploadSelector) { (0, parserHelpers_1.addLog)(`[UploadParser] Could not determine target selector from "${uploadMatch[2].trim()}". Failing parse for upload.`); return null; } (0, parserHelpers_1.addLog)(`[ParseSuccess] Action: upload, FilePath: "${filePathValue}", Target: "${finalUploadSelector}"`); return { action: 'upload', filePath: filePathValue, target: finalUploadSelector, originalStep: originalStepInput }; } return null; } static _tryParseDragAndDropAction(currentStep, originalStepInput) { (0, parserHelpers_1.addLog)(`[DEBUG_DND] Entered _tryParseDragAndDropAction. currentStep: "${currentStep}"`); const dragAndDropPattern = /(?:drag|move)\s+(.+?)\s+to\s+(.+)/i; const dndMatch = currentStep.match(dragAndDropPattern); (0, parserHelpers_1.addLog)(`[DEBUG_DND] dndMatch result: ${JSON.stringify(dndMatch)}`); if (dndMatch && dndMatch[1] && dndMatch[2]) { const sourceRaw = dndMatch[1].trim(); const destinationRaw = dndMatch[2].trim(); (0, parserHelpers_1.addLog)(`[DEBUG_DND] sourceRaw: "${sourceRaw}", destinationRaw: "${destinationRaw}"`); // Source hint: const sourceHintResult = (0, parserHelpers_1.extractHintedSelector)(sourceRaw); (0, parserHelpers_1.addLog)(`[DEBUG_DND] sourceHintResult: ${JSON.stringify(sourceHintResult)}`); const sourceSelector = sourceHintResult.selectorValue ? (0, parserHelpers_1.finalizeSelector)(str(sourceHintResult.type), str(sourceHintResult.selectorValue)) : (0, parserHelpers_1.extractQuotedText)(sourceRaw) ?? sourceRaw; // Destination hint: const destinationHintResult = (0, parserHelpers_1.extractHintedSelector)(destinationRaw); (0, parserHelpers_1.addLog)(`[DEBUG_DND] destinationHintResult: ${JSON.stringify(destinationHintResult)}`); const destinationSelector = destinationHintResult.selectorValue ? (0, parserHelpers_1.finalizeSelector)(str(destinationHintResult.type), str(destinationHintResult.selectorValue)) : (0, parserHelpers_1.extractQuotedText)(destinationRaw) ?? destinationRaw; (0, parserHelpers_1.addLog)(`[DEBUG_DND] Final sourceSelector: "${sourceSelector}", Final destinationSelector: "${destinationSelector}"`); return { action: 'dragAndDrop', target: sourceSelector, destinationTarget: destinationSelector, originalStep: originalStepInput }; } (0, parserHelpers_1.addLog)(`[DEBUG_DND] Pattern did not match or not enough groups.`); return null; } static _tryParseAssertions(currentStep, originalStepInput) { // 1) URL assertion: const assertUrlPattern = /^(?:assert|verify|check|expect)(?: that)? current url (is|equals|contains) (['"])(.*?)\2/i; const urlAssertMatch = currentStep.match(assertUrlPattern); if (urlAssertMatch) { (0, parserHelpers_1.addLog)(`[AssertURL] Matched: "${urlAssertMatch[0]}"`); const capturedCondition = urlAssertMatch[1].toLowerCase(); const finalCondition = (capturedCondition === 'is' ? 'equals' : capturedCondition); const expectedText = urlAssertMatch[3]; return { action: 'assert', originalStep: originalStepInput, assertion: { type: 'url', condition: finalCondition, expectedText: expectedText ?? '' } }; } // 2) Structured assertion: assert "(type=..., selector=..., expected=..., condition=...)" const structuredAssertPattern = /^(?:assert|verify|check|expect)\s+(['"])(\s*type\s*=\s*([^,]+?)\s*,\s*selector\s*=\s*([^,]+?)\s*,\s*expected\s*=\s*([^,]*?)\s*,\s*condition\s*=\s*([^,]+?)\s*)\1/i; const structuredAssertMatch = currentStep.match(structuredAssertPattern); if (structuredAssertMatch && structuredAssertMatch[2]) { (0, parserHelpers_1.addLog)(`[StructuredAssert] Matched: "${structuredAssertMatch[0]}"`); const assertType = (structuredAssertMatch[3] ?? '').trim(); const selector = (structuredAssertMatch[4] ?? '').trim(); const expectedText = (structuredAssertMatch[5] ?? '').trim() === 'N/A' ? '' : (structuredAssertMatch[5] ?? '').trim(); const condition = (structuredAssertMatch[6] ?? '').trim(); if (!assertType || !selector || !condition) { (0, parserHelpers_1.addLog)(`[StructuredAssert] Failed to parse essential parts from: "${structuredAssertMatch[2]}"`); return null; } (0, parserHelpers_1.addLog)(`[StructuredAssert] Parsed - Type: ${assertType}, Selector: ${selector}, Expected: ${expectedText}, Condition: ${condition}`); return { action: 'assert', originalStep: originalStepInput, assertion: { type: assertType, selector: selector, expectedText: expectedText, condition: condition } }; } // 3) General element assertion: "assert element <selector> [text] is visible/hidden/exists/etc" const generalAssertionPattern = /^(?:assert|verify|check|expect)(?: that)?\s+(?:(page)|(element|selector)\s+((?:\\(.+?\\)|[^\\s'"])+|'[^']+'|"[^"]+")(?:\s+(text))?\s+(is visible|is hidden|is not visible|exists|does not exist|is|equals|contains)(?:\s+(['"])(.*?)\7)?)$/i; const assertionMatch = currentStep.match(generalAssertionPattern); (0, parserHelpers_1.addLog)(`[AssertGeneralElement] Attempting to match: "${currentStep}"`); (0, parserHelpers_1.addLog)(`[AssertGeneralElement] Match result: ${JSON.stringify(assertionMatch)}`); if (assertionMatch) { let assertionType = 'elementVisible'; let condition = 'isVisible'; let expectedText; let selector; if (assertionMatch[1]) { // "page" group matched → pageText assertion assertionType = 'pageText'; condition = assertionMatch[6].toLowerCase() === 'contains' ? 'contains' : 'equals'; expectedText = assertionMatch[8] ?? ''; } else if (assertionMatch[2]) { // "element" or "selector" group matched selector = assertionMatch[3]; selector = (0, parserHelpers_1.extractQuotedText)(selector) ?? selector; (0, parserHelpers_1.addLog)(`[AssertGeneralElement] Extracted selector: "${selector}"`); if (assertionMatch[5]) { // "text" keyword present → elementText assertion assertionType = 'elementText'; condition = assertionMatch[6].toLowerCase() === 'contains' ? 'contains' : 'equals'; expectedText = assertionMatch[8] ?? ''; } else { // No "text" → visibility/exists const conditionText = assertionMatch[6].toLowerCase(); if (conditionText === 'is visible') { assertionType = 'elementVisible'; condition = 'isVisible'; expectedText = 'true'; } else if (conditionText === 'is hidden' || conditionText === 'is not visible') { assertionType = 'elementVisible'; condition = 'isVisible'; expectedText = 'false'; } else if (conditionText === 'exists' || conditionText === 'does not exist') { assertionType = 'elementVisible'; condition = 'isVisible'; expectedText = conditionText === 'exists' ? 'true' : 'false'; } else { // Fallback default to "visible = true" assertionType = 'elementVisible'; condition = 'isVisible'; expectedText = 'true'; } } } return { action: 'assert', originalStep: originalStepInput, assertion: { type: assertionType, selector: selector ?? '', expectedText: expectedText ?? '', condition: condition } }; } // Special case: "assert that the page contains \"text\"" const pageContainsPattern = /^(?:assert|verify|check|expect)(?: that)?\s+the page\s+(?:contains|includes)\s+(['"])(.*?)\1/i; const pageContainsMatch = currentStep.match(pageContainsPattern); if (pageContainsMatch) { const expectedText = pageContainsMatch[2]; (0, parserHelpers_1.addLog)(`[AssertPageContains] Matched. Expected text="${expectedText}"`); return { action: 'assert', originalStep: originalStepInput, assertion: { type: 'pageText', condition: 'contains', expectedText } }; } return null; } static _tryParseExecuteScriptAction(normalizedStep, originalStepInput) { const executeScriptPattern = /execute javascript in frame\s+(.+?)\s+with script\s+(.+?)(?:\s+(and expect (alert|confirm|prompt)(?: with text ['"](.*?)['"])? then (accept|dismiss)))?$/i; const match = originalStepInput.match(executeScriptPattern); if (match && match[1] && match[2]) { let frameSelector = match[1].trim(); let script = match[2].trim(); const dialogKeywordOuter = match[3]; const matchedDialogType = match[4]; const matchedPromptText = match[5]; const matchedDialogAction = match[6]; frameSelector = (0, parserHelpers_1.extractQuotedText)(frameSelector) || frameSelector; script = (0, parserHelpers_1.extractQuotedText)(script) || script; if (!frameSelector || !script) { (0, parserHelpers_1.addLog)('[ParseWarning] ExecuteScript: Frame selector or script is empty after trim/unquote.'); return null; } if (!/^\((css|xpath):\s*.+\)$/i.test(frameSelector)) { (0, parserHelpers_1.addLog)(`[ParseWarning] ExecuteScript: Frame selector "${frameSelector}" is not in the expected (type: value) format.`); } let dialogExpectation = undefined; if (dialogKeywordOuter && matchedDialogType && matchedDialogAction) { const type = matchedDialogType.toLowerCase(); const action = matchedDialogAction.toLowerCase(); if ((type === 'alert' || type === 'confirm' || type === 'prompt') && (action === 'accept' || action === 'dismiss')) { dialogExpectation = { type: type, response: action === 'accept' ? true : false }; if (type === 'prompt' && action === 'accept' && matchedPromptText) { dialogExpectation.response = matchedPromptText; } (0, parserHelpers_1.addLog)(`[ExecuteScript] Directly parsed dialog expectation: ${JSON.stringify(dialogExpectation)}`); } else { (0, parserHelpers_1.addLog)(`[ExecuteScript] Parsed dialog type/action ("${type}", "${action}") are not valid.`); } } else if (dialogKeywordOuter) { (0, parserHelpers_1.addLog)(`[ExecuteScript] Potential dialog phrase "${dialogKeywordOuter}" was incomplete or not recognized by the refined regex.`); } const scriptLogValue = script.length > 100 ? script.substring(0, 100) + '...' : script; (0, parserHelpers_1.addLog)(`[ParseSuccess] Action: executeScript, FrameSelector: "${frameSelector}", Script: "${scriptLogValue}", Dialog: ${dialogExpectation ? 'Yes' : 'No'}`); return { action: 'executeScript', target: frameSelector, value: script, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } return null; } static _tryParseTypeAction(currentStep, originalStepInput) { const typePattern = /type\s+\(([^)]+)\)\s+with\s+value\s+(['"])(.*?)\2/i; const typeMatch = currentStep.match(typePattern); if (typeMatch && typeMatch[1] && typeMatch[3]) { const selector = typeMatch[1].trim(); const value = typeMatch[3]; (0, parserHelpers_1.addLog)(`[TypeParser] Matched AI-suggested type action. Selector: ${selector}, Value: ${value}`); return { action: 'type', target: selector, value: value, originalStep: originalStepInput }; } return null; } static _parseStandardActionsAndFallbacks(currentStep, normalizedCurrentStep, originalStepInput, mainHintedSelectorType, mainHintedSelectorValue, dialogExpectation) { // // 1) If after stripping off any "hint" + any "dialog expectation" there is no text left, // but we had a "main hinted selector," then assume "click" on that main hint: // if (!currentStep && mainHintedSelectorValue) { const safeMainType = mainHintedSelectorType ?? 'generic'; const safeMainValue = mainHintedSelectorValue ?? ''; (0, parserHelpers_1.addLog)(`Step became empty after hint and dialog processing, ` + `assuming 'click' on hinted target: "${safeMainValue}".`); return { action: 'click', target: (0, parserHelpers_1.finalizeSelector)(str(safeMainType), str(safeMainValue)), originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 2) "navigate/go to/open/visit" pattern // if ((0, parserHelpers_1.matchesPattern)(normalizedCurrentStep, ['navigate', 'go to', 'open', 'visit', 'launch', 'start', 'access', 'load'])) { const url = (0, parserHelpers_1.extractQuotedText)(currentStep) || (0, parserHelpers_1.extractUrl)(currentStep) || mainHintedSelectorValue || ''; (0, parserHelpers_1.addLog)(`[ParseNavigate] Original step: "${originalStepInput}", ` + `Current step for URL: "${currentStep}", Extracted URL: "${url}"`); return { action: 'navigate', target: url, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 3) "type/enter/fill in/input/write/set ... (value) ... in/into/to/on (selector)" // const typeOrFillPattern = /^(?:type|enter|fill (?:in )?|input|write|set)(?: text| value)?\s+(.+?)(?:\s+(?:in|into|to|on)\s+(.+))?$/i; const typeMatch = currentStep.match(typeOrFillPattern); const typeAltPattern = /^(?:type|enter|fill|input|write|set)(?: text| value)?\s+([^\s]+)\s+with\s+(.+)/i; const typeMatchAlt = currentStep.match(typeAltPattern); if (typeMatch || typeMatchAlt) { (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Matched type pattern. Groups: ${JSON.stringify(typeMatch)}`); const rawValueToType = typeMatch ? typeMatch[1].trim() : typeMatchAlt[2].trim(); const rawTargetField = typeMatch ? typeMatch[2]?.trim() : typeMatchAlt[1].trim(); let valueToType = (0, parserHelpers_1.extractQuotedText)(rawValueToType) ?? rawValueToType; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Raw value: "${rawValueToType}", Extracted value: "${valueToType}"`); // If it looks like an email or contains '@', pull out just the email if (valueToType.includes('@') || valueToType.toLowerCase().includes('email')) { const emailMatch = valueToType.match(/([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/); if (emailMatch) { valueToType = emailMatch[0]; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Extracted email: "${valueToType}" from "${rawValueToType}"`); } } else if (/^(?:a\s+)?(?:valid|correct)\s+password$/i.test(valueToType)) { // Placeholder for valid/correct password if (process.env.LABNEX_VALID_PASSWORD) { valueToType = process.env.LABNEX_VALID_PASSWORD; } else { valueToType = '__PROMPT_VALID_PASSWORD__'; } (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Placeholder 'valid/correct password' detected.`); } else if (valueToType.toLowerCase().includes('password')) { // If it looks like "password fooBar!", capture just fooBar! const passwordMatch = valueToType.match(/(?:password\s+)([^\s].*?)$/i); if (passwordMatch && passwordMatch[1]) { valueToType = passwordMatch[1]; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Extracted password: "${valueToType}" from "${rawValueToType}"`); } } else if (valueToType.toLowerCase().startsWith('username ')) { valueToType = valueToType.replace(/^username\s+/i, ''); (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Stripped 'username' prefix. New value: "${valueToType}"`); } else if (valueToType.toLowerCase().startsWith('first name ')) { valueToType = valueToType.replace(/^first name\s+/i, ''); (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Stripped 'first name' prefix. New value: "${valueToType}"`); } else if (valueToType.toLowerCase().startsWith('last name ')) { valueToType = valueToType.replace(/^last name\s+/i, ''); (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Stripped 'last name' prefix. New value: "${valueToType}"`); } else if (valueToType.toLowerCase().startsWith('zip ')) { valueToType = valueToType.replace(/^zip\s+/i, ''); (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Stripped 'zip' prefix. New value: "${valueToType}"`); if (/^\d{1,4}$/.test(valueToType)) { const needed = 5 - valueToType.length; valueToType = valueToType + ''.padEnd(needed, '0'); (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Padded ZIP to 5 digits: "${valueToType}"`); } } else if (/^a\s+(valid|correct)\s+username$/i.test(valueToType) || /^(valid|correct)\s+username$/i.test(valueToType)) { if (process.env.LABNEX_VALID_USERNAME) { valueToType = process.env.LABNEX_VALID_USERNAME; } else { valueToType = '__PROMPT_VALID_USERNAME__'; } (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Placeholder 'valid username' detected.`); } let finalTargetSelector; if (rawTargetField) { (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Raw target field specified: "${rawTargetField}"`); const targetHintResult = (0, parserHelpers_1.extractHintedSelector)(rawTargetField); if (targetHintResult.selectorValue) { const safeType = str(targetHintResult.type); const safeSelectorValue = str(targetHintResult.selectorValue); finalTargetSelector = (0, parserHelpers_1.finalizeSelector)(safeType, safeSelectorValue); if (!finalTargetSelector) { finalTargetSelector = 'default-selector'; } (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Target field parsed as hint. Type: ${safeType}, Final: "${finalTargetSelector}"`); } else { // No hint, so either quoted selector or generic phrases const unquotedTarget = (0, parserHelpers_1.extractQuotedText)(rawTargetField) || rawTargetField; const lowerTarg = unquotedTarget.toLowerCase(); if (lowerTarg.includes('username')) { finalTargetSelector = 'input[id*="user" i], input[name*="user" i], input[placeholder*="user" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred username selector from target field: "${finalTargetSelector}"`); } else if (lowerTarg.includes('password')) { finalTargetSelector = 'input[type="password"], #password, input[name="password"], input[placeholder*="password" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred password selector from target field: "${finalTargetSelector}"`); } else { finalTargetSelector = (0, parserHelpers_1.extractGeneralSelector)(unquotedTarget) || unquotedTarget; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Target field parsed as non-hint. Using: "${finalTargetSelector}"`); } } } else if (mainHintedSelectorValue) { (0, parserHelpers_1.addLog)(`[TypeActionAttempt] No explicit target field. Using pre-parsed main hint selector.`); const safeMainType = str(mainHintedSelectorType); const safeMainValue = str(mainHintedSelectorValue); finalTargetSelector = (0, parserHelpers_1.finalizeSelector)(safeMainType, safeMainValue || 'default-selector'); } else { (0, parserHelpers_1.addLog)(`[TypeActionAttempt] No explicit target field and no main hint. Inferring target based on value content.`); if (valueToType.toLowerCase().includes('email') || valueToType.includes('@')) { finalTargetSelector = 'input[type="email"], #email, input[name="email"], input[placeholder*="email" i], input[id*="user" i], input[name*="user" i], input[placeholder*="user" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred email input target: "${finalTargetSelector}"`); } else if (valueToType.toLowerCase().includes('password') || rawValueToType.toLowerCase().includes('password')) { finalTargetSelector = 'input[type="password"], #password, input[name="password"], input[placeholder*="password" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred password input target: "${finalTargetSelector}"`); } else { const lowerOriginal = originalStepInput.toLowerCase(); if (lowerOriginal.includes('first name')) { finalTargetSelector = 'input[id*="first" i], input[name*="first" i], input[placeholder*="first" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred first-name input selector: "${finalTargetSelector}"`); } else if (lowerOriginal.includes('last name')) { finalTargetSelector = 'input[id*="last" i], input[name*="last" i], input[placeholder*="last" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred last-name input selector: "${finalTargetSelector}"`); } else if (lowerOriginal.includes('zip') || lowerOriginal.includes('postal')) { finalTargetSelector = 'input[id*="zip" i], input[name*="zip" i], input[placeholder*="zip" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred zip input selector: "${finalTargetSelector}"`); } else if (lowerOriginal.includes('username') || lowerOriginal.includes('user name')) { finalTargetSelector = 'input[id*="user" i], input[name*="user" i], input[placeholder*="user" i]'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] Inferred username input selector: "${finalTargetSelector}"`); } else { finalTargetSelector = 'input, textarea'; (0, parserHelpers_1.addLog)(`[TypeActionAttempt] No specific content match, using generic input target: "${finalTargetSelector}"`); } } } // If the user literally typed "" or '', treat it as an empty string if (valueToType === '""' || valueToType === "''") { valueToType = ""; } (0, parserHelpers_1.addLog)(`[ParseSuccess] Action: type, Value: "${valueToType}", Target: "${finalTargetSelector}"`); return { action: 'type', value: valueToType, // We used a non-null assertion (!) because every branch above guarantees it's a non‐undefined string target: finalTargetSelector, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 4) "click/tap/press/select on the <ordinal?> <target> [button|link|...]" // const clickPattern = /^(?:click|tap|press|select)(?: on)?(?: the)?\s+((?:first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|\d+(?:st|nd|rd|th)?)?\s+)?(.+?)(?:\s+(?:button|link|element|field|checkbox|radio|option|tab|item))?$/i; const clickMatch = currentStep.match(clickPattern); if (clickMatch) { (0, parserHelpers_1.addLog)(`[ClickActionAttempt] Matched click pattern. Groups: ${JSON.stringify(clickMatch)}`); const positionText = clickMatch[1]?.trim() || ''; const rawTarget = clickMatch[2].trim(); let index = 0; if (positionText) { if (/^\d+/.test(positionText)) { index = parseInt(positionText, 10) - 1; // Convert to 0-based index } else { const ordinalMap = { first: 0, second: 1, third: 2, fourth: 3, fifth: 4, sixth: 5, seventh: 6, eighth: 7, ninth: 8, tenth: 9 }; index = ordinalMap[positionText.toLowerCase()] ?? 0; } } (0, parserHelpers_1.addLog)(`[ClickActionAttempt] Position: ${positionText || 'none'}, Index: ${index}, Raw Target: "${rawTarget}"`); // Try extracting a hinted selector const hintResult = (0, parserHelpers_1.extractHintedSelector)(rawTarget); let finalTargetSelector = ''; if (hintResult.selectorValue) { // Coalesce hintResult.type and hintResult.selectorValue into real strings const safeType = str(hintResult.type); const safeSelectorValue = str(hintResult.selectorValue); const tempSelector = (0, parserHelpers_1.finalizeSelector)(safeType, safeSelectorValue); finalTargetSelector = tempSelector || 'default-selector'; (0, parserHelpers_1.addLog)(`[ClickActionAttempt] Using hinted selector: "${finalTargetSelector}"`); } else { const unquotedTarget = (0, parserHelpers_1.extractQuotedText)(rawTarget) ?? rawTarget; // If it's a "checkbox" text, pick a generic checkbox input if (unquotedTarget.toLowerCase().includes('checkbox')) { finalTargetSelector = 'input[type="checkbox"]'; (0, parserHelpers_1.addLog)(`[ClickActionAttempt] Inferred checkbox selector: "${finalTargetSelector}"`); } else { finalTargetSelector = unquotedTarget; (0, parserHelpers_1.addLog)(`[ClickActionAttempt] Using unquoted/raw target as selector: "${finalTargetSelector}"`); } } return { action: 'click', target: finalTargetSelector, index: index, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 5) If none of the above matched, but we do have a "main hinted selector," click it: // if (mainHintedSelectorValue) { const safeMainType = str(mainHintedSelectorType); const safeMainValue = str(mainHintedSelectorValue); (0, parserHelpers_1.addLog)(`No specific action matched after main hint. ` + `Assuming 'click' on hinted target: "${safeMainValue}".`); return { action: 'click', target: (0, parserHelpers_1.finalizeSelector)(safeMainType, safeMainValue || 'default-selector'), originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 6) Last fallback: treat the entire step text as the thing to "click": // const unquotedStep = (0, parserHelpers_1.extractQuotedText)(originalStepInput) || originalStepInput; if (unquotedStep) { (0, parserHelpers_1.addLog)(`No specific action or hint matched. Treating entire step "${unquotedStep}" as a click target (broad fallback).`); const broadXPath = `xpath:(//*[self::a or self::button or self::input[@type='button'] or self::input[@type='submit']][contains(normalize-space(.), "${unquotedStep}")] | //*[@aria-label="${unquotedStep}"] | //*[contains(normalize-space(@placeholder), "${unquotedStep}")])[1]`; return { action: 'click', target: broadXPath, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } // // 7) If we truly can't parse anything, default to clicking the raw step string: // (0, parserHelpers_1.addLog)(`[ParseFail] Could not determine action for step: "${originalStepInput}". Defaulting to click with original step as target.`); return { action: 'click', target: originalStepInput, originalStep: originalStepInput, expectsDialog: dialogExpectation }; } static parseStep(step) { (0, parserHelpers_1.addLog)(`[ParseStep] Parsing step: "${step}"`); const normalizedStep = step.trim().replace(/\s+/g, ' '); if (/^\s*skip\b/i.test(normalizedStep)) { return { action: 'skip', originalStep: step }; } let result = null; // 0) Check if it's already the AI "type (css: ...) with value" format: result = TestStepParser._tryParseTypeAction(step, step); if (result) { (0, parserHelpers_1.addLog)(`[ParseStep] Successfully parsed as AI-suggested type action.`); return result; } const originalStepInput = step; let currentStep = step.trim(); let normalizedCurrentStep = currentStep.toLowerCase(); (0, parserHelpers_1.addLog)(`[ParseAttempt] Original: "${originalStepInput}", Normalized: "${normalizedCurrentStep}"`); // 1) "wait/pause for X seconds" const waitAction = TestStepParser._tryParseWaitAction(normalizedCurrentStep, originalStepInput); if (waitAction) { (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _tryParseWaitAction. Target: ${waitAction.target}`); return waitAction; } // 2) "switch to iframe ..." or "switch to main content" const iframeAction = TestStepParser._tryParseIframeSwitch(currentStep, normalizedCurrentStep, originalStepInput); if (iframeAction) { (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _tryParseIframeSwitch. Target: ${iframeAction.target}`); return iframeAction; } // 3) "upload/attach file to ..." const uploadAction = TestStepParser._tryParseUploadAction(currentStep, originalStepInput); if (uploadAction) { (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _tryParseUploadAction. Target: ${uploadAction.target}`); return uploadAction; } // 4) "drag <source> to <destination>" const dragAndDropAction = TestStepParser._tryParseDragAndDropAction(currentStep, originalStepInput); if (dragAndDropAction) { (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _tryParseDragAndDropAction. ` + `Target: ${dragAndDropAction.target}, DestTarget: ${dragAndDropAction.destinationTarget}`); return dragAndDropAction; } // 5) Various "assert" patterns const assertionAction = TestStepParser._tryParseAssertions(currentStep, originalStepInput); if (assertionAction) { (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _tryParseAssertions. ` + `Assertion Selector: ${assertionAction.assertion?.selector}`); return assertionAction; } // 6) "execute javascript in frame ... with script ... (and expect dialog)" const executeScriptAction = TestStepParser._tryParseExecuteScriptAction(normalizedCurrentStep, originalStepInput); if (executeScriptAction) { const scriptLogValue = executeScriptAction.value ? executeScriptAction.value.length > 70 ? executeScriptAction.value.substring(0, 70) + '...' : executeScriptAction.value : 'undefined'; (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] Parsed executeScript action. ` + `Frame: ${executeScriptAction.target}, Script: ${scriptLogValue}`); return executeScriptAction; } // // 7) Extract a "main hinted selector" if the user wrote something like "(css: #foo) click foo" // let mainHintedSelectorType; let mainHintedSelectorValue; const hintResult = (0, parserHelpers_1.extractHintedSelector)(currentStep); if (hintResult.selectorValue) { mainHintedSelectorType = hintResult.type; mainHintedSelectorValue = hintResult.selectorValue; currentStep = hintResult.remainingStep; normalizedCurrentStep = currentStep.toLowerCase().trim(); (0, parserHelpers_1.addLog)(`[MainHintExtraction] Found hint. Type: ${mainHintedSelectorType}, ` + `Value: ${mainHintedSelectorValue}. Remaining step: "${currentStep}"`); } // // 8) Extract any dialog expectation (like "then accept alert") // const dialogResult = (0, parserHelpers_1.extractDialogExpectation)(currentStep); const dialogExpectation = dialogResult.expectation; currentStep = dialogResult.remainingStep; normalizedCurrentStep = currentStep.toLowerCase().trim(); // Narrative navigation phrases like "Launch the application..." or "Start the website..." const narrativeNavRegex = /^(launch|start|open|access|load)\b.+(application|website|app)\b/i; if (narrativeNavRegex.test(originalStepInput) && !(0, parserHelpers_1.extractUrl)(originalStepInput)) { (0, parserHelpers_1.addLog)(`[NarrativeNavigate] Detected high-level navigation step. Treating as navigate to base URL.`); return { action: 'navigate', target: '', // Will be resolved to baseUrl in executor originalStep: originalStepInput, expectsDialog: undefined }; } // // 9) Everything else falls through to "standard actions + fallback": // const standardOrFallbackAction = TestStepParser._parseStandardActionsAndFallbacks(currentStep, normalizedCurrentStep, originalStepInput, mainHintedSelectorType, mainHintedSelectorValue, dialogExpectation); const returnedTarget = standardOrFallbackAction.target; (0, parserHelpers_1.addLog)(`[TestStepParser.parseStep] After _parseStandardActionsAndFallbacks. ` + `standardOrFallbackAction.target: "${returnedTarget}" (Length: ${returnedTarget?.length})`); return standardOrFallbackAction; } } exports.TestStepParser = TestStepParser; //# sourceMappingURL=testStepParser.js.map