UNPKG

smartui-migration-tool

Version:

Enterprise-grade CLI tool for migrating visual testing platforms to LambdaTest SmartUI

1,033 lines 48.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CodeTransformer = void 0; const parser_1 = require("@babel/parser"); const traverse_1 = __importDefault(require("@babel/traverse")); const generator_1 = __importDefault(require("@babel/generator")); const t = __importStar(require("@babel/types")); /** * CodeTransformer module for transforming test code from visual testing platforms to SmartUI * Uses Abstract Syntax Trees (AST) for safe and accurate code transformation */ class CodeTransformer { constructor(projectPath) { this.projectPath = projectPath; } /** * Transforms C# code from various platforms to SmartUI format * @param sourceCode - The source code to transform * @param platform - The platform to transform from (Percy, Applitools, Sauce Labs) * @param framework - The framework being used (Playwright, Selenium) * @returns CodeTransformationResult - Transformation result with content, warnings, and snapshot count */ transformCSharp(sourceCode, platform, framework) { const warnings = []; let snapshotCount = 0; let transformedCode = sourceCode; try { // Transform based on platform switch (platform) { case 'Applitools': transformedCode = this.transformApplitoolsCSharp(sourceCode, framework, warnings); break; case 'Percy': transformedCode = this.transformPercyCSharp(sourceCode, framework, warnings); break; case 'Sauce Labs': transformedCode = this.transformSauceLabsCSharp(sourceCode, framework, warnings); break; default: warnings.push({ message: `Unsupported platform for C# transformation: ${platform}`, line: 0 }); } // Count snapshots in the transformed code snapshotCount = this.countSnapshots(transformedCode); return { content: transformedCode, warnings, snapshotCount }; } catch (error) { warnings.push({ message: `C# transformation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, line: 0 }); return { content: sourceCode, warnings, snapshotCount: 0 }; } } /** * Transforms Percy JavaScript/TypeScript code to SmartUI format * @param sourceCode - The source code to transform * @returns CodeTransformationResult - Transformation result with content, warnings, and snapshot count */ transformPercy(sourceCode) { const warnings = []; let snapshotCount = 0; try { // Parse the source code into an AST const ast = (0, parser_1.parse)(sourceCode, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'objectRestSpread', 'functionBind', 'exportDefaultFrom', 'typescript', 'jsx', 'decorators-legacy', 'classProperties', 'asyncGenerators', 'functionSent', 'throwExpressions' ] }); // Traverse and transform the AST const self = this; (0, traverse_1.default)(ast, { // Transform import declarations ImportDeclaration(path) { self.transformPercyImport(path, warnings); }, // Transform require() calls CallExpression(path) { if (t.isIdentifier(path.node.callee, { name: 'require' })) { self.transformPercyRequire(path, warnings); } else if (t.isIdentifier(path.node.callee, { name: 'percySnapshot' })) { self.transformPercySnapshot(path, warnings); snapshotCount++; } }, // Transform destructured require calls VariableDeclarator(path) { if (path.node.init && t.isCallExpression(path.node.init) && t.isIdentifier(path.node.init.callee, { name: 'require' })) { self.transformPercyRequire(path.node.init, warnings); } // Transform variable names from percySnapshot to smartuiSnapshot if (t.isIdentifier(path.node.id, { name: 'percySnapshot' })) { path.node.id.name = 'smartuiSnapshot'; } } }); // Generate the transformed code const result = (0, generator_1.default)(ast, { retainLines: false, compact: false, comments: true }); return { content: result.code, warnings, snapshotCount }; } catch (error) { // Handle parsing errors const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error'; warnings.push({ message: `Failed to parse source code: ${errorMessage}`, details: 'The source file may contain unsupported syntax or be malformed.' }); return { content: sourceCode, // Return original code on error warnings, snapshotCount: 0 }; } } /** * Transforms Percy import declarations to SmartUI imports * @param path - The AST path for the import declaration * @param warnings - Array to add warnings to */ transformPercyImport(path, warnings) { const source = path.node.source.value; // Map Percy SDK imports to SmartUI equivalents const importMappings = { '@percy/cypress': '@lambdatest/smartui-cypress', '@percy/playwright': '@lambdatest/smartui-playwright', '@percy/storybook': '@lambdatest/smartui-storybook', '@percy/selenium-webdriver': '@lambdatest/smartui-selenium', '@percy/puppeteer': '@lambdatest/smartui-puppeteer' }; if (importMappings[source]) { path.node.source.value = importMappings[source]; } } /** * Transforms Percy require() calls to SmartUI requires * @param path - The AST path for the require call * @param warnings - Array to add warnings to */ transformPercyRequire(path, warnings) { if (path.node && path.node.arguments && path.node.arguments.length > 0 && t.isStringLiteral(path.node.arguments[0])) { const source = path.node.arguments[0].value; // Map Percy SDK requires to SmartUI equivalents const requireMappings = { '@percy/cypress': '@lambdatest/smartui-cypress', '@percy/playwright': '@lambdatest/smartui-playwright', '@percy/storybook': '@lambdatest/smartui-storybook', '@percy/selenium-webdriver': '@lambdatest/smartui-selenium', '@percy/puppeteer': '@lambdatest/smartui-puppeteer' }; if (requireMappings[source]) { path.node.arguments[0].value = requireMappings[source]; } } } /** * Transforms percySnapshot calls to smartuiSnapshot calls * @param path - The AST path for the percySnapshot call * @param warnings - Array to add warnings to */ transformPercySnapshot(path, warnings) { // Change the function name from percySnapshot to smartuiSnapshot path.node.callee.name = 'smartuiSnapshot'; // Transform options if present (options is typically the last argument) if (path.node.arguments.length > 2) { const optionsArg = path.node.arguments[2]; if (t.isObjectExpression(optionsArg)) { this.transformPercySnapshotOptions(optionsArg, warnings, path); } } else if (path.node.arguments.length === 2) { const optionsArg = path.node.arguments[1]; if (t.isObjectExpression(optionsArg)) { this.transformPercySnapshotOptions(optionsArg, warnings, path); } } } /** * Transforms Percy snapshot options to SmartUI options * @param optionsNode - The AST node for the options object * @param warnings - Array to add warnings to * @param snapshotPath - The AST path for the snapshot call (for adding comments) */ transformPercySnapshotOptions(optionsNode, warnings, snapshotPath) { const newProperties = []; let hasWidthsWarning = false; optionsNode.properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { const key = prop.key.name; switch (key) { case 'widths': // widths is not supported in SmartUI - generate warning and skip this property hasWidthsWarning = true; warnings.push({ message: 'Per-snapshot `widths` option was found and is not supported. Viewports must be configured in `.smartui.json`.', details: 'Configure viewports globally in your SmartUI configuration file instead of per-snapshot.' }); // Don't add this property to newProperties (skip it) break; case 'ignore_region_selectors': // Map to SmartUI ignoreDOM.cssSelector if (t.isArrayExpression(prop.value)) { newProperties.push(t.objectProperty(t.identifier('ignoreDOM'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), prop.value) ]))); } break; case 'scope': // Map to SmartUI element.cssSelector if (t.isStringLiteral(prop.value)) { newProperties.push(t.objectProperty(t.identifier('element'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), prop.value) ]))); } break; default: // Keep other properties as-is newProperties.push(prop); break; } } else { // Keep non-identifier properties as-is newProperties.push(prop); } }); // Update the options object with transformed properties optionsNode.properties = newProperties; // Add warning comment if widths was found if (hasWidthsWarning) { this.addWarningComment(snapshotPath); } } /** * Adds a warning comment above the snapshot call * @param snapshotPath - The AST path for the snapshot call */ addWarningComment(snapshotPath) { const warningComment = t.addComment(snapshotPath.node, 'leading', ' MIGRATION-WARNING: per-snapshot widths are not supported. Please configure viewports in your .smartui.json file.', false); } /** * Transforms Applitools JavaScript/TypeScript code to SmartUI format * @param sourceCode - The source code to transform * @param framework - The testing framework (Cypress or Playwright) * @returns CodeTransformationResult - Transformation result with content, warnings, and snapshot count */ transformApplitools(sourceCode, framework) { const warnings = []; let snapshotCount = 0; try { // Parse the source code into an AST const ast = (0, parser_1.parse)(sourceCode, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'objectRestSpread', 'functionBind', 'exportDefaultFrom', 'typescript', 'jsx', 'decorators-legacy', 'classProperties', 'asyncGenerators', 'functionSent', 'throwExpressions' ] }); // Traverse and transform the AST const self = this; const snapshotCountRef = { count: 0 }; (0, traverse_1.default)(ast, { // Transform import declarations ImportDeclaration(path) { self.transformApplitoolsImport(path, warnings); }, // Transform require() calls CallExpression(path) { if (t.isIdentifier(path.node.callee, { name: 'require' })) { self.transformApplitoolsRequire(path, warnings); } }, // Transform Applitools API calls ExpressionStatement(path) { let callNode = null; if (t.isCallExpression(path.node.expression)) { callNode = path.node.expression; } else if (t.isAwaitExpression(path.node.expression) && t.isCallExpression(path.node.expression.argument)) { callNode = path.node.expression.argument; } if (callNode) { // Handle eyes.open() and eyes.close() calls - remove them if (self.isEyesOpenCall(callNode) || self.isEyesCloseCall(callNode)) { path.remove(); return; } // Handle eyes.check() calls - transform to smartuiSnapshot if (self.isEyesCheckCall(callNode)) { self.transformEyesCheckToSmartUISnapshot(callNode, warnings, framework); snapshotCountRef.count++; } } }, // Transform Applitools API calls in variable declarations VariableDeclarator(path) { if (path.node.init && t.isCallExpression(path.node.init)) { const callNode = path.node.init; // Handle eyes.open() and eyes.close() calls - remove them if (self.isEyesOpenCall(callNode) || self.isEyesCloseCall(callNode)) { path.remove(); return; } // Handle eyes.check() calls - transform to smartuiSnapshot if (self.isEyesCheckCall(callNode)) { self.transformEyesCheckToSmartUISnapshot(callNode, warnings, framework); snapshotCountRef.count++; } } // Transform variable names from eyes to smartui if (t.isIdentifier(path.node.id, { name: 'eyes' })) { path.node.id.name = 'smartui'; } } }); snapshotCount = snapshotCountRef.count; // Generate the transformed code const result = (0, generator_1.default)(ast, { retainLines: false, compact: false, comments: true }); return { content: result.code, warnings, snapshotCount }; } catch (error) { // Handle parsing errors const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error'; warnings.push({ message: `Failed to parse source code: ${errorMessage}`, details: 'The source file may contain unsupported syntax or be malformed.' }); return { content: sourceCode, // Return original code on error warnings, snapshotCount: 0 }; } } /** * Transforms Applitools import declarations to SmartUI imports * @param path - The AST path for the import declaration * @param warnings - Array to add warnings to */ transformApplitoolsImport(path, warnings) { const source = path.node.source.value; // Map Applitools SDK imports to SmartUI equivalents const importMappings = { '@applitools/eyes-cypress': '@lambdatest/smartui-cypress', '@applitools/eyes-playwright': '@lambdatest/smartui-playwright', '@applitools/eyes-selenium': '@lambdatest/smartui-selenium', '@applitools/eyes-puppeteer': '@lambdatest/smartui-puppeteer' }; if (importMappings[source]) { path.node.source.value = importMappings[source]; } } /** * Transforms Applitools require() calls to SmartUI requires * @param path - The AST path for the require call * @param warnings - Array to add warnings to */ transformApplitoolsRequire(path, warnings) { if (path.node && path.node.arguments && path.node.arguments.length > 0 && t.isStringLiteral(path.node.arguments[0])) { const source = path.node.arguments[0].value; // Map Applitools SDK requires to SmartUI equivalents const requireMappings = { '@applitools/eyes-cypress': '@lambdatest/smartui-cypress', '@applitools/eyes-playwright': '@lambdatest/smartui-playwright', '@applitools/eyes-selenium': '@lambdatest/smartui-selenium', '@applitools/eyes-puppeteer': '@lambdatest/smartui-puppeteer' }; if (requireMappings[source]) { path.node.arguments[0].value = requireMappings[source]; } } } /** * Checks if a call is an eyes.open() call * @param callNode - The AST node for the call expression * @returns True if this is an eyes.open() call */ isEyesOpenCall(callNode) { return ((t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'eyes' }) && t.isIdentifier(callNode.callee.property, { name: 'open' })) || (t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'cy' }) && t.isIdentifier(callNode.callee.property, { name: 'eyesOpen' }))); } /** * Checks if a call is an eyes.close() call * @param callNode - The AST node for the call expression * @returns True if this is an eyes.close() call */ isEyesCloseCall(callNode) { return ((t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'eyes' }) && (t.isIdentifier(callNode.callee.property, { name: 'close' }) || t.isIdentifier(callNode.callee.property, { name: 'closeAsync' }))) || (t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'cy' }) && t.isIdentifier(callNode.callee.property, { name: 'eyesClose' }))); } /** * Checks if a call is an eyes.check() call * @param callNode - The AST node for the call expression * @returns True if this is an eyes.check() call */ isEyesCheckCall(callNode) { return ((t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'eyes' }) && t.isIdentifier(callNode.callee.property, { name: 'check' })) || (t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'cy' }) && t.isIdentifier(callNode.callee.property, { name: 'eyesCheckWindow' }))); } /** * Transforms eyes.check() calls to smartuiSnapshot calls * @param callNode - The AST node for the eyes.check() call * @param warnings - Array to add warnings to * @param framework - The testing framework */ transformEyesCheckToSmartUISnapshot(callNode, warnings, framework) { // Extract snapshot name and options from the eyes.check() call const { snapshotName, options, isLayout } = this.parseEyesCheckArguments(callNode, warnings); // Create the smartuiSnapshot call const smartUISnapshotCall = this.createSmartUISnapshotCall(snapshotName, options, framework); // If this is a layout region, inject functional assertion if (isLayout) { this.injectLayoutEmulation(callNode, smartUISnapshotCall, framework, warnings); } // Replace the original call callNode.callee = t.identifier('smartuiSnapshot'); callNode.arguments = smartUISnapshotCall.arguments; } /** * Parses arguments from eyes.check() calls * @param callNode - The AST node for the eyes.check() call * @param warnings - Array to add warnings to * @returns Object containing snapshot name, options, and layout flag */ parseEyesCheckArguments(callNode, warnings) { let snapshotName = 'Untitled Snapshot'; let options = {}; let isLayout = false; if (callNode.arguments.length > 0) { // First argument is typically the snapshot name or target const firstArg = callNode.arguments[0]; if (t.isStringLiteral(firstArg)) { snapshotName = firstArg.value; } else if (t.isCallExpression(firstArg)) { // Handle Target.window() or Target.region() calls const targetCall = firstArg; if (t.isMemberExpression(targetCall.callee) && t.isIdentifier(targetCall.callee.object, { name: 'Target' })) { if (t.isIdentifier(targetCall.callee.property, { name: 'window' })) { // Target.window() - no special options needed } else if (t.isIdentifier(targetCall.callee.property, { name: 'region' })) { // Target.region() - extract selector if (targetCall.arguments.length > 0 && t.isStringLiteral(targetCall.arguments[0])) { options.element = { cssSelector: targetCall.arguments[0].value }; } } else if (t.isIdentifier(targetCall.callee.property, { name: 'layout' })) { // Target.layout() - this is a layout region isLayout = true; if (targetCall.arguments.length > 0 && t.isStringLiteral(targetCall.arguments[0])) { options.layoutSelector = targetCall.arguments[0].value; callNode._layoutSelector = targetCall.arguments[0].value; } } } } } // Parse additional arguments for options if (callNode.arguments.length > 1) { const optionsArg = callNode.arguments[1]; if (t.isStringLiteral(optionsArg)) { snapshotName = optionsArg.value; } else if (t.isObjectExpression(optionsArg)) { this.parseApplitoolsOptions(optionsArg, options, warnings); } } return { snapshotName, options, isLayout }; } /** * Parses Applitools options object * @param optionsNode - The AST node for the options object * @param options - The options object to populate * @param warnings - Array to add warnings to */ parseApplitoolsOptions(optionsNode, options, warnings) { optionsNode.properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { const key = prop.key.name; switch (key) { case 'ignore': // Handle ignore regions if (t.isArrayExpression(prop.value)) { const ignoreSelectors = prop.value.elements .filter((element) => t.isStringLiteral(element)) .map((element) => element.value); if (ignoreSelectors.length > 0) { options.ignoreDOM = { cssSelector: ignoreSelectors }; } } break; case 'fully': // Handle fully() modifier (can be boolean true or function call) if (t.isBooleanLiteral(prop.value) && prop.value.value === true) { warnings.push({ message: 'Applitools `fully()` was detected. To achieve full-page screenshots in SmartUI, please ensure your viewports in `.smartui.json` are defined with a single width value (e.g., `[1920]`).', details: 'Configure viewports globally in your SmartUI configuration file for full-page screenshots.' }); } else if (t.isCallExpression(prop.value) && t.isIdentifier(prop.value.callee, { name: 'fully' })) { warnings.push({ message: 'Applitools `fully()` was detected. To achieve full-page screenshots in SmartUI, please ensure your viewports in `.smartui.json` are defined with a single width value (e.g., `[1920]`).', details: 'Configure viewports globally in your SmartUI configuration file for full-page screenshots.' }); } break; default: // Keep other properties as-is break; } } }); } /** * Creates a smartuiSnapshot call AST node * @param snapshotName - The name of the snapshot * @param options - The options object * @param framework - The testing framework * @returns The AST node for the smartuiSnapshot call */ createSmartUISnapshotCall(snapshotName, options, framework) { const args = [t.stringLiteral(snapshotName)]; // Add options if present if (Object.keys(options).length > 0) { const optionsObject = t.objectExpression([]); if (options.ignoreDOM) { optionsObject.properties.push(t.objectProperty(t.identifier('ignoreDOM'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), t.arrayExpression(options.ignoreDOM.cssSelector.map((selector) => t.stringLiteral(selector)))) ]))); } if (options.element) { optionsObject.properties.push(t.objectProperty(t.identifier('element'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), t.stringLiteral(options.element.cssSelector)) ]))); } args.push(optionsObject); } return t.callExpression(t.identifier('smartuiSnapshot'), args); } /** * Injects layout emulation with functional assertions * @param originalCall - The original eyes.check() call * @param smartUISnapshotCall - The new smartuiSnapshot call * @param framework - The testing framework * @param warnings - Array to add warnings to */ injectLayoutEmulation(originalCall, smartUISnapshotCall, framework, warnings) { const layoutSelector = originalCall._layoutSelector; if (!layoutSelector) return; // Add warning comment const warningComment = t.addComment(originalCall, 'leading', ' MIGRATION-NOTE: Applitools \'layout\' region was emulated. A functional assertion was added to check for the container\'s visibility, and a SmartUI snapshot was taken with child elements ignored. Please verify this provides adequate coverage.', false); // Create functional assertion based on framework let assertionCall; if (framework === 'Playwright') { assertionCall = t.awaitExpression(t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.identifier('expect'), t.identifier('toBeVisible')), [t.callExpression(t.memberExpression(t.identifier('page'), t.identifier('locator')), [t.stringLiteral(layoutSelector)])]), t.identifier('toBeVisible')), [])); } else { // Cypress assertionCall = t.callExpression(t.memberExpression(t.callExpression(t.memberExpression(t.identifier('cy'), t.identifier('get')), [t.stringLiteral(layoutSelector)]), t.identifier('should')), [t.stringLiteral('be.visible')]); } // Modify the smartuiSnapshot call to ignore child elements if (smartUISnapshotCall.arguments.length > 1) { const optionsArg = smartUISnapshotCall.arguments[1]; if (t.isObjectExpression(optionsArg)) { // Add ignoreDOM for child elements optionsArg.properties.push(t.objectProperty(t.identifier('ignoreDOM'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), t.arrayExpression([t.stringLiteral(`${layoutSelector} *`)])) ]))); } } // Store the assertion to be inserted before the snapshot originalCall._layoutAssertion = assertionCall; } /** * Transforms Sauce Labs Visual JavaScript/TypeScript code to SmartUI format * @param sourceCode - The source code to transform * @returns CodeTransformationResult - Transformation result with content, warnings, and snapshot count */ transformSauceLabs(sourceCode) { const warnings = []; let snapshotCount = 0; try { // Parse the source code into an AST const ast = (0, parser_1.parse)(sourceCode, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'objectRestSpread', 'functionBind', 'exportDefaultFrom', 'typescript', 'jsx', 'decorators-legacy', 'classProperties', 'asyncGenerators', 'functionSent', 'throwExpressions' ] }); // Traverse and transform the AST const self = this; const snapshotCountRef = { count: 0 }; (0, traverse_1.default)(ast, { // Transform import declarations ImportDeclaration(path) { self.transformSauceLabsImport(path, warnings); }, // Transform require() calls CallExpression(path) { if (t.isIdentifier(path.node.callee, { name: 'require' })) { self.transformSauceLabsRequire(path, warnings); } }, // Transform Sauce Labs API calls ExpressionStatement(path) { let callNode = null; if (t.isCallExpression(path.node.expression)) { callNode = path.node.expression; } else if (t.isAwaitExpression(path.node.expression) && t.isCallExpression(path.node.expression.argument)) { callNode = path.node.expression.argument; } if (callNode && self.isSauceVisualCheckCall(callNode)) { self.transformSauceVisualCheckToSmartUISnapshot(callNode, warnings); snapshotCountRef.count++; } }, // Transform Sauce Labs API calls in variable declarations VariableDeclarator(path) { if (path.node.init && t.isCallExpression(path.node.init) && self.isSauceVisualCheckCall(path.node.init)) { self.transformSauceVisualCheckToSmartUISnapshot(path.node.init, warnings); snapshotCountRef.count++; } // Transform variable names from sauce to smartui if (t.isIdentifier(path.node.id, { name: 'sauce' })) { path.node.id.name = 'smartui'; } } }); snapshotCount = snapshotCountRef.count; // Generate the transformed code const result = (0, generator_1.default)(ast, { retainLines: false, compact: false, comments: true }); return { content: result.code, warnings, snapshotCount }; } catch (error) { // Handle parsing errors const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error'; warnings.push({ message: `Failed to parse source code: ${errorMessage}`, details: 'The source file may contain unsupported syntax or be malformed.' }); return { content: sourceCode, // Return original code on error warnings, snapshotCount: 0 }; } } /** * Transforms Sauce Labs import declarations to SmartUI imports * @param path - The AST path for the import declaration * @param warnings - Array to add warnings to */ transformSauceLabsImport(path, warnings) { const source = path.node.source.value; // Map Sauce Labs SDK imports to SmartUI equivalents const importMappings = { '@saucelabs/cypress-plugin': '@lambdatest/smartui-cypress', '@saucelabs/webdriverio': '@lambdatest/smartui-selenium', '@saucelabs/playwright-plugin': '@lambdatest/smartui-playwright' }; if (importMappings[source]) { path.node.source.value = importMappings[source]; } } /** * Transforms Sauce Labs require() calls to SmartUI requires * @param path - The AST path for the require call * @param warnings - Array to add warnings to */ transformSauceLabsRequire(path, warnings) { if (path.node && path.node.arguments && path.node.arguments.length > 0 && t.isStringLiteral(path.node.arguments[0])) { const source = path.node.arguments[0].value; // Map Sauce Labs SDK requires to SmartUI equivalents const requireMappings = { '@saucelabs/cypress-plugin': '@lambdatest/smartui-cypress', '@saucelabs/webdriverio': '@lambdatest/smartui-selenium', '@saucelabs/playwright-plugin': '@lambdatest/smartui-playwright' }; if (requireMappings[source]) { path.node.arguments[0].value = requireMappings[source]; } } } /** * Checks if a call is a sauceVisualCheck call * @param callNode - The AST node for the call expression * @returns True if this is a sauceVisualCheck call */ isSauceVisualCheckCall(callNode) { return ((t.isIdentifier(callNode.callee, { name: 'sauceVisualCheck' })) || (t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'cy' }) && t.isIdentifier(callNode.callee.property, { name: 'sauceVisualCheck' })) || (t.isMemberExpression(callNode.callee) && t.isIdentifier(callNode.callee.object, { name: 'browser' }) && t.isIdentifier(callNode.callee.property, { name: 'sauceVisualCheck' }))); } /** * Transforms sauceVisualCheck calls to smartuiSnapshot calls * @param callNode - The AST node for the sauceVisualCheck call * @param warnings - Array to add warnings to */ transformSauceVisualCheckToSmartUISnapshot(callNode, warnings) { // Extract snapshot name and options from the sauceVisualCheck call const { snapshotName, options } = this.parseSauceVisualCheckArguments(callNode, warnings); // Create the smartuiSnapshot call const smartUISnapshotCall = this.createSmartUISnapshotCallFromSauceLabs(snapshotName, options); // Replace the original call callNode.callee = t.identifier('smartuiSnapshot'); callNode.arguments = smartUISnapshotCall.arguments; } /** * Parses arguments from sauceVisualCheck calls * @param callNode - The AST node for the sauceVisualCheck call * @param warnings - Array to add warnings to * @returns Object containing snapshot name and options */ parseSauceVisualCheckArguments(callNode, warnings) { let snapshotName = 'Untitled Snapshot'; let options = {}; if (callNode.arguments.length > 0) { // First argument is typically the snapshot name const firstArg = callNode.arguments[0]; if (t.isStringLiteral(firstArg)) { snapshotName = firstArg.value; } } // Parse additional arguments for options if (callNode.arguments.length > 1) { const optionsArg = callNode.arguments[1]; if (t.isObjectExpression(optionsArg)) { this.parseSauceLabsOptions(optionsArg, options, warnings); } } return { snapshotName, options }; } /** * Parses Sauce Labs options object * @param optionsNode - The AST node for the options object * @param options - The options object to populate * @param warnings - Array to add warnings to */ parseSauceLabsOptions(optionsNode, options, warnings) { optionsNode.properties.forEach((prop) => { if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) { const key = prop.key.name; switch (key) { case 'ignoredRegions': // Handle ignored regions if (t.isArrayExpression(prop.value)) { const ignoreSelectors = prop.value.elements .filter((element) => t.isStringLiteral(element)) .map((element) => element.value); if (ignoreSelectors.length > 0) { options.ignoreDOM = { cssSelector: ignoreSelectors }; } } break; case 'clipSelector': // Handle clip selector if (t.isStringLiteral(prop.value)) { options.element = { cssSelector: prop.value.value }; } break; case 'captureDom': // Handle capture DOM flag - SmartUI always captures DOM, so this can be ignored // No warning needed as this is a compatible feature break; case 'diffingMethod': case 'diffingOptions': // Handle unsupported diffing options warnings.push({ message: 'Sauce Labs\' custom `diffingMethod` and `diffingOptions` are not supported by SmartUI. The snapshot will be compared using SmartUI\'s default algorithm. Please review the results carefully.', details: 'SmartUI uses its own comparison algorithm. Consider adjusting your test expectations if needed.' }); break; default: // Keep other properties as-is break; } } }); } /** * Creates a smartuiSnapshot call AST node from Sauce Labs options * @param snapshotName - The name of the snapshot * @param options - The options object * @returns The AST node for the smartuiSnapshot call */ createSmartUISnapshotCallFromSauceLabs(snapshotName, options) { const args = [t.stringLiteral(snapshotName)]; // Add options if present if (Object.keys(options).length > 0) { const optionsObject = t.objectExpression([]); if (options.ignoreDOM) { optionsObject.properties.push(t.objectProperty(t.identifier('ignoreDOM'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), t.arrayExpression(options.ignoreDOM.cssSelector.map((selector) => t.stringLiteral(selector)))) ]))); } if (options.element) { optionsObject.properties.push(t.objectProperty(t.identifier('element'), t.objectExpression([ t.objectProperty(t.identifier('cssSelector'), t.stringLiteral(options.element.cssSelector)) ]))); } args.push(optionsObject); } return t.callExpression(t.identifier('smartuiSnapshot'), args); } // C# transformation methods /** * Transform Applitools C# code to SmartUI */ transformApplitoolsCSharp(sourceCode, framework, warnings) { let transformedCode = sourceCode; // Transform using statements transformedCode = transformedCode.replace(/using\s+Applitools[^;]*;/g, 'using LambdaTest.SmartUI;'); // Transform Eyes class declarations (private Eyes _eyes;) transformedCode = transformedCode.replace(/private\s+Eyes\s+_eyes;/g, 'private SmartUI _eyes;'); // Transform Eyes variable declarations transformedCode = transformedCode.replace(/Eyes\s+_eyes\s*=/g, 'SmartUI _eyes ='); // Transform eyes initialization transformedCode = transformedCode.replace(/_eyes\s*=\s*new\s+Eyes\(\);/g, '_eyes = new SmartUI();'); // Transform eyes.OpenAsync() calls transformedCode = transformedCode.replace(/await\s+_eyes\.OpenAsync\([^)]+\);/g, '// SmartUI initialization handled automatically'); // Transform eyes.Check() calls to SmartUI snapshots transformedCode = transformedCode.replace(/_eyes\.Check\(Target\.Window\(\)\.Fully\(\)\.WithName\(["']([^"']+)["']\)\);/g, (match, snapshotName) => { if (framework === 'Playwright') { return `SmartUISnapshot.SmartUISnapshot(_page, "${snapshotName}");`; } else { return `SmartUISnapshot.SmartUISnapshot(_page, "${snapshotName}");`; } }); // Transform eyes.CloseAsync() calls transformedCode = transformedCode.replace(/await\s+_eyes\.CloseAsync\(\);/g, '// SmartUI cleanup handled automatically'); return transformedCode; } /** * Transform Percy C# code to SmartUI */ transformPercyCSharp(sourceCode, framework, warnings) { let transformedCode = sourceCode; // Transform using statements transformedCode = transformedCode.replace(/using\s+Percy[^;]*;/g, 'using LambdaTest.SmartUI;'); // Transform percy.snapshot() calls transformedCode = transformedCode.replace(/percy\.snapshot\(["']([^"']+)["']\)/g, (match, snapshotName) => { if (framework === 'Playwright') { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } else { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } }); // Transform Percy.snapshot() calls transformedCode = transformedCode.replace(/Percy\.snapshot\(["']([^"']+)["']\)/g, (match, snapshotName) => { if (framework === 'Playwright') { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } else { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } }); return transformedCode; } /** * Transform Sauce Labs C# code to SmartUI */ transformSauceLabsCSharp(sourceCode, framework, warnings) { let transformedCode = sourceCode; // Transform using statements transformedCode = transformedCode.replace(/using\s+SauceLabs[^;]*;/g, 'using LambdaTest.SmartUI;'); // Transform sauce.screenshot() calls transformedCode = transformedCode.replace(/sauce\.screenshot\(["']([^"']+)["']\)/g, (match, snapshotName) => { if (framework === 'Playwright') { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } else { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } }); // Transform SauceLabs.screenshot() calls transformedCode = transformedCode.replace(/SauceLabs\.screenshot\(["']([^"']+)["']\)/g, (match, snapshotName) => { if (framework === 'Playwright') { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } else { return `SmartUISnapshot.SmartUISnapshot(driver, "${snapshotName}");`; } }); return transformedCode; } /** * Count snapshots in transformed code */ countSnapshots(code) { const snapshotPatterns = [ /SmartUISnapshot\.SmartUISnapshot\(/g, /smartuiSnapshot\(/g ]; let count = 0; for (const pattern of snapshotPatterns) { const matches = code.match(pattern); if (matches) { count += matches.length; } } return count; } } exports.CodeTransformer = CodeTransformer; //# sourceMappingURL=CodeTransformer.js.map