UNPKG

babel-plugin-react-native-testid

Version:

babel plugin for react native testid attributes

232 lines 8.58 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const t = __importStar(require("@babel/types")); function nameForReactComponent(path) { const { parentPath } = path; if (!t.isArrowFunctionExpression(path.node) && t.isIdentifier(path.node.id)) { return path.node.id; } if (t.isVariableDeclarator(parentPath)) { // @ts-ignore return parentPath.node.id; } return null; } const DEFAULT_ATTRIBUTE = 'testID'; const DEFAULT_ATTRIBUTE_ID = 'id'; const DEFAULT_RN_IGNORE_ELEMENTS = [ 'View', 'Text', 'Image', 'ScrollView', 'FlatList', 'SectionList', 'TouchableOpacity', 'TouchableHighlight', 'TouchableWithoutFeedback', 'SafeAreaView', 'Modal', 'Pressable', 'ActivityIndicator', 'Fragment', ]; // 默认的有意义属性 const DEFAULT_MEANINGFUL_ATTRIBUTES = [ 'title', 'placeholder', 'label', 'alt', 'name', 'id', 'key', 'accessibilityLabel', ]; function createDataAttribute(name, attributeName) { return t.jsxAttribute(t.jsxIdentifier(attributeName), t.stringLiteral(name)); } function hasDataAttribute(node, attributeName) { // This is a robust check. It looks for the specific attribute. // Crucially, if it finds a spread attribute ({...props}), it assumes the // attribute *might* be present in the spread and returns true to be safe. // This prevents the plugin from adding a duplicate testID. let hasSpread = false; for (const attribute of node.attributes) { if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: attributeName })) { return true; // Found exact attribute } if (t.isJSXSpreadAttribute(attribute)) { hasSpread = true; } } return hasSpread; } function getAttributeValue(node, attributeName) { const attribute = node.attributes.find((attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: attributeName })); if (!attribute || !t.isJSXAttribute(attribute)) { return null; } if (t.isStringLiteral(attribute.value)) { return attribute.value.value; } return null; } function extractTextFromJSXElement(element) { const children = element.children; for (const child of children) { if (t.isJSXText(child)) { const text = child.value.trim(); if (text) { return text; } } else if (t.isJSXExpressionContainer(child)) { // 处理 {t('key')} 这样的i18n调用 const expression = child.expression; if (t.isCallExpression(expression)) { // 检查是否是 t() 调用 if (t.isIdentifier(expression.callee, { name: 't' }) && expression.arguments.length > 0) { const firstArg = expression.arguments[0]; if (t.isStringLiteral(firstArg)) { return firstArg.value; } } // 检查是否是 i18n.t() 调用 if (t.isMemberExpression(expression.callee) && t.isIdentifier(expression.callee.property, { name: 't' }) && expression.arguments.length > 0) { const firstArg = expression.arguments[0]; if (t.isStringLiteral(firstArg)) { return firstArg.value; } } } } else if (t.isJSXElement(child)) { // 递归处理嵌套元素 const text = extractTextFromJSXElement(child); if (text) { return text; } } } return null; } function generateTestId(openingElement, jsxElement, currentElementName, parentChain, delimiter, meaningfulAttributes, testIdAttributes) { // 1. 最高优先级:手动指定的testID for (const testIdAttr of testIdAttributes) { if (hasDataAttribute(openingElement, testIdAttr)) { return null; // 已有testID,不需要生成 } } // 3. 核心:从有意义的属性中提取 for (const meaningfulAttr of meaningfulAttributes) { const meaningfulValue = getAttributeValue(openingElement, meaningfulAttr); if (meaningfulValue) { // 对于name属性,添加前缀 if (meaningfulAttr === 'name') { return `${currentElementName.toLowerCase()}-${meaningfulValue}`; } return meaningfulValue; } } // 4. 核心:从组件内容中提取 const textContent = extractTextFromJSXElement(jsxElement); if (textContent) { return textContent; } // 5. 最低优先级:回退到基于组件名的层级结构 return parentChain ? `${parentChain}${delimiter}${currentElementName}` : currentElementName; } function addTestIdToElement(openingElement, testId, attributes) { for (const attributeName of attributes) { if (!hasDataAttribute(openingElement, attributeName)) { const dataAttribute = createDataAttribute(testId, attributeName); openingElement.attributes.push(dataAttribute); } } } function processElementTestId(p, currentElementName, currentChain, s) { const { attributes, delimiter, meaningfulAttributes, } = s; const openingElement = p.get('openingElement'); const testId = generateTestId(openingElement.node, p.node, currentElementName, currentChain, delimiter, meaningfulAttributes, attributes); if (testId) { addTestIdToElement(openingElement.node, testId, attributes); } } const chainingVisitor = { JSXFragment(p, s) { p.traverse(chainingVisitor, s); p.skip(); }, JSXElement(p, s) { const { parentChain, delimiter, ignoreElements } = s; const openingElement = p.get('openingElement'); let currentElementName = null; if (t.isJSXIdentifier(openingElement.node.name)) { currentElementName = openingElement.node.name.name; } if (!currentElementName) { p.traverse(chainingVisitor, s); p.skip(); return; } const isIgnored = ignoreElements.includes(currentElementName); const currentChain = isIgnored ? parentChain : parentChain ? `${parentChain}${delimiter}${currentElementName}` : currentElementName; if (!isIgnored) { processElementTestId(p, currentElementName, currentChain, s); } p.traverse(chainingVisitor, Object.assign(Object.assign({}, s), { parentChain: currentChain })); p.skip(); }, }; function plugin() { return { name: 'react-native-testid-intelligent', visitor: { 'FunctionExpression|ArrowFunctionExpression|FunctionDeclaration': (path, state) => { var _a; const componentIdentifier = nameForReactComponent(path); if (!componentIdentifier) { return; } const componentName = componentIdentifier.name; if (!/^[A-Z]/u.test(componentName)) { return; } const { attributes = [DEFAULT_ATTRIBUTE, DEFAULT_ATTRIBUTE_ID], delimiter = '-', ignoreElements = DEFAULT_RN_IGNORE_ELEMENTS, meaningfulAttributes = DEFAULT_MEANINGFUL_ATTRIBUTES, } = (_a = state.opts) !== null && _a !== void 0 ? _a : {}; const visitorState = { parentChain: componentName, attributes, delimiter, ignoreElements, meaningfulAttributes, }; const body = path.get('body'); if (body.isBlockStatement()) { body.traverse(chainingVisitor, visitorState); } else if (body.isJSXElement() || body.isJSXFragment()) { body.traverse(chainingVisitor, visitorState); } }, }, }; } exports.default = plugin; //# sourceMappingURL=index.js.map