vite项目使用 Vue3 自定义指令为特定元素添加 UID:提高 UI 自动化测试的稳定性

背景介绍

在 UI 自动化测试中,选择器的不稳定性经常会导致测试用例的失败。传统的 XPath 选择器在 HTML 结构变化时容易失效,增加了维护成本。为了提高测试的稳定性和降低维护成本,我们可以为元素添加自定义的 UID 属性,作为稳定的选择器。在本文中,我将详细介绍如何通过 Vue3 的自定义指令为特定元素添加 UID 属性。

亮点

在实现过程中,我们需要计算 loc.start.offset 等数值,以确保 UID 属性正确地插入到元素的属性中。当时(2021)网上几乎没有相关的参考资料,这个方案完全是通过自己对源码的研究和理解总结出来的。

代码实现

ini 复制代码
const crypto = require('crypto');

// 生成哈希值
const generateHash = (value) => {
  return crypto.createHash('md5').update(value).digest('hex').slice(0, 6);
};

// 创建 UID 属性对象
const createUIDAttribute = (name, val, line, locStartColumn, locStartOffset) => {
  const locEndColumn = locStartColumn + name.length + val.length + 3;
  const locEndOffset = locStartOffset + name.length + val.length + 3;
  const valueLocStartColumn = locStartColumn + name.length + 1;
  const valueLocStartOffset = locStartOffset + name.length + 1;

  return {
    type: 6,
    name: name,
    value: {
      type: 2,
      content: val,
      loc: {
        start: {
          column: valueLocStartColumn,
          line: line,
          offset: valueLocStartOffset,
        },
        end: {
          column: locEndColumn,
          line: line,
          offset: locEndOffset,
        },
        source: `"${val}"`,
      },
    },
    loc: {
      start: {
        column: locStartColumn,
        line: line,
        offset: locStartOffset,
      },
      end: {
        column: locEndColumn,
        line: line,
        offset: locEndOffset,
      },
      source: `${name}="${val}"`,
    },
  };
};

// 更新绑定的 key 属性
const updateBindKeyProperty = (bindKeyProp, newBindKeyProp) => {
  if (bindKeyProp?.loc) {
    bindKeyProp.loc.source = newBindKeyProp?.loc?.source;
    bindKeyProp.loc.start = newBindKeyProp?.loc?.start;
    bindKeyProp.loc.end = newBindKeyProp?.loc?.end;
  }
  if (bindKeyProp.exp) {
    bindKeyProp.exp.loc.source = newBindKeyProp.exp?.loc?.source;
    bindKeyProp.exp.loc.start = newBindKeyProp.exp?.loc?.start;
    bindKeyProp.exp.loc.end = newBindKeyProp.exp?.loc?.end;
    let divide = undefined;
    if (bindKeyProp.exp.children) {
      bindKeyProp.exp.children.forEach((item) => {
        if (item && item?.loc) {
          if (divide === undefined) {
            divide = newBindKeyProp.exp?.loc?.start?.column - item?.loc?.start?.column || 0;
          }
          item.loc.start.column += divide;
          item.loc.start.offset += divide;
          item.loc.end.column += divide;
          item.loc.end.offset += divide;
        }
      });
      bindKeyProp.exp.children.push(`+ '${newBindKeyProp.exp.loc.source.slice(-7)}'`);
    }
  }
  if (bindKeyProp.arg) {
    bindKeyProp.arg.loc.source = newBindKeyProp.arg?.loc?.source;
    bindKeyProp.arg.loc.start = newBindKeyProp.arg?.loc?.start;
    bindKeyProp.arg.loc.end = newBindKeyProp.arg?.loc?.end;
    bindKeyProp.arg.content = newBindKeyProp.arg?.content;
  }
};

const nodeTransformsFn = [
  (el) => {
    const oldProps = el.props;
    const oldLastNodeProp = oldProps && oldProps[oldProps.length - 1];
    let modelVal = '',
      onVal = '',
      bindKey = '';

    // 遍历元素的旧属性,查找特定的绑定属性
    if (oldLastNodeProp) {
      const oldPropsLen = oldProps.length;
      let bindKeyProp = null;
      for (let i = 0; i < oldPropsLen; i++) {
        const oldProp = oldProps[i];
        if (oldProp.name === 'bind' && oldProp.arg.content === 'key') {
          bindKey = oldProp.exp?.loc?.source;
          bindKeyProp = JSON.parse(JSON.stringify(oldProp));
        }
        if (oldProp.name === 'model' && !modelVal) {
          modelVal = oldProp.exp?.loc?.source;
        }
        if (oldProp.name === 'on' && !onVal) {
          onVal = oldProp.exp?.loc?.source;
        }
      }

      const oldLastLoc = oldLastNodeProp?.loc;
      const oldLastLocEnd = oldLastLoc.end;
      const line = oldLastLocEnd.line;
      const locStartColumn = oldLastLocEnd.column + 1;
      const locStartOffset = oldLastLocEnd.offset + 1;

      if (modelVal || onVal) {
        try {
          const name = 'uid';
          let val = modelVal ? modelVal : onVal;

          // 使用 md5 哈希生成唯一值
          val = generateHash(val);
          val = modelVal ? `m${val}` : `o${val}`;

          // 创建并添加 UID 属性
          const uidAttr = createUIDAttribute(name, val, line, locStartColumn, locStartOffset);
          if (!bindKey) el.props.push(uidAttr);
        } catch (error) {
          console.error(error);
        }
      }

      if (bindKey) {
        const name = 'key';
        const locSource = bindKeyProp?.loc?.source;
        const expLocSource = bindKeyProp.exp?.loc?.source;
        let expLocSourceStr = modelVal || onVal || expLocSource;

        // 使用 md5 哈希生成唯一值并拼接
        expLocSourceStr = generateHash(expLocSourceStr);
        expLocSourceStr = 'f' + expLocSourceStr;
        const addStr = "+'" + expLocSourceStr + "'";
        const newExpLocSourceStr = expLocSource + addStr;
        const newLocSource = locSource.replace(expLocSource, newExpLocSourceStr);

        const newBindKeyProp = {
          exp: {
            loc: {
              source: newExpLocSourceStr,
              start: {
                line,
                column: locStartColumn + name.length + 3,
                offset: locStartOffset + name.length + 3,
              },
              end: {
                line,
                column: locStartColumn + name.length + 3 + newExpLocSourceStr.length,
                offset: locStartOffset + name.length + 3 + newExpLocSourceStr.length,
              },
            },
            children: [],
          },
          arg: {
            content: 'uid',
            loc: {
              source: 'uid',
              start: {
                line,
                column: locStartColumn + 1,
                offset: locStartOffset + 1,
              },
              end: {
                line,
                column: locStartColumn + 1 + 3,
                offset: locStartOffset + 1 + 3,
              },
            },
          },
          loc: {
            start: {
              line,
              column: locStartColumn,
              offset: locStartOffset,
            },
            end: {
              line,
              column: locStartColumn + newLocSource.length,
              offset: locStartOffset + newLocSource.length,
            },
            source: newLocSource,
          },
        };

        // 更新绑定的 key 属性
        updateBindKeyProperty(bindKeyProp, newBindKeyProp);
        el.props.push(bindKeyProp);
      }
    }
  },
];

export default ({ mode }) =>
  defineConfig({
    base: './',
    plugins: [
      vue({
        reactivityTransform: true,
        template: {
          compilerOptions: {
            nodeTransforms: [...nodeTransformsFn],
          },
        },
      }),
    ],
  });

代码解析

  • 获取旧属性 :首先,我们获取元素的旧属性,并遍历它们以查找 modelon 属性的值。

  • 计算位置偏移 :通过计算 loc.start.offset 和其他位置相关的值,我们可以确保在正确的位置插入新的 UID 属性。

  • 生成 UID :使用 crypto 库生成一个基于 md5 的唯一值,并将其设置为 uid 属性的值。

  • 更新属性:最后,我们将新的属性添加到元素的属性列表中。

注意事项

  • 对于大型项目或有大量 DOM 元素的页面,遍历和计算每个元素的属性可能会导致性能问题。 优化建议:仅为关键元素添加 UID 属性,避免对所有元素进行操作。
  • 使用 md5 生成唯一值存在潜在的哈希冲突风险,尽管概率较小,但仍需注意。
  • 使用 crypto 库在某些环境中可能需要额外配置,增加了项目的依赖和配置复杂性。 优化建议:确保项目环境和依赖管理良好,必要时可以引入 polyfill 或替代方案。

结论

通过这种方法,我们可以为元素添加唯一的 UID 属性,作为 UI 自动化测试中的选择器。相比于传统的 XPath 选择器,这种方法更为稳定和可靠,减少了维护成本。然而,在实际应用中,我们需要结合项目的具体情况进行优化和调整,以确保方案的有效性和高效性。希望这篇文章对你有所帮助,如果有任何问题或建议,欢迎在评论区留言。

相关推荐
fury_12326 分钟前
当大的div中有六个小的div,上面三个下面三个,当外层div高变大的时候我希望里面的小的div的高也变大
前端·javascript·html
大鸡腿最好吃40 分钟前
为啥react要用jsx
前端·javascript·react.js
小黄编程快乐屋44 分钟前
前端小练习——大雪纷飞(JS没有上限!!!)
开发语言·前端·javascript
程序猿阿伟1 小时前
《平衡之策:C++应对人工智能不平衡训练数据的数据增强方法》
前端·javascript·c++
STUPID MAN1 小时前
vue3使用后端传递的文件流进行文件预览
前端·javascript·vue.js·文件预览
-代号95271 小时前
【React】二、状态变量useState
前端·javascript·react.js
爬坑的小白1 小时前
el-menu导航三级数据结构及数据展示
前端·javascript·vue.js
CodeSheep1 小时前
雷军又添一员猛将!!
前端·后端·程序员
dz88i82 小时前
关于Chrome自动同步书签的解决办法
前端·chrome
温轻舟2 小时前
前端开发 之 15个页面加载特效中【附完整源码】
前端·javascript·css·html