背景介绍
在 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],
},
},
}),
],
});
代码解析
-
获取旧属性 :首先,我们获取元素的旧属性,并遍历它们以查找
model
和on
属性的值。 -
计算位置偏移 :通过计算
loc.start.offset
和其他位置相关的值,我们可以确保在正确的位置插入新的 UID 属性。 -
生成 UID :使用
crypto
库生成一个基于md5
的唯一值,并将其设置为uid
属性的值。 -
更新属性:最后,我们将新的属性添加到元素的属性列表中。
注意事项
- 对于大型项目或有大量 DOM 元素的页面,遍历和计算每个元素的属性可能会导致性能问题。 优化建议:仅为关键元素添加 UID 属性,避免对所有元素进行操作。
- 使用
md5
生成唯一值存在潜在的哈希冲突风险,尽管概率较小,但仍需注意。 - 使用
crypto
库在某些环境中可能需要额外配置,增加了项目的依赖和配置复杂性。 优化建议:确保项目环境和依赖管理良好,必要时可以引入 polyfill 或替代方案。
结论
通过这种方法,我们可以为元素添加唯一的 UID 属性,作为 UI 自动化测试中的选择器。相比于传统的 XPath 选择器,这种方法更为稳定和可靠,减少了维护成本。然而,在实际应用中,我们需要结合项目的具体情况进行优化和调整,以确保方案的有效性和高效性。希望这篇文章对你有所帮助,如果有任何问题或建议,欢迎在评论区留言。