- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
上一章我们完成了UI元素及元素页面的管理功能,这一章节将实现UI自动化用例的编辑管理功能和执行功能。
完整教程地址:《从0搭建自动化测试平台》
项目在线演示地址:http://121.43.43.59/ (帐号:admin 密码:123456)
本章内容实现效果如下:
编辑用例
执行用例(需要执行客户端!联系博主获取)
npx umi g page uiCases/index --typescript
增加提示和跳转功能
我们可以使用antd的Tooltip
组件对卡片内容进行包裹,然后增加点击跳转到uicase路由的事件即可,代码如下:
<Tooltip placement="top" title="点我查看UI用例">
<ul style={{ listStyleType: 'none', cursor: 'pointer', padding: 0 }} className={styles.card} onClick={() => history.push('/case/uiCaseOverview/UiCases')}>
<Badge status="error" text="用例失败数量:" />
{item.status1}
<li>
<Badge status="default" text="等待执行数量:" />
{item.status0}
</li>
<li>
<Badge status="warning" text="正在执行数量:" />
{item.status2}
</li>
<li>
<Badge status="processing" text="执行完成数量:" />
{item.status3}
</li>
<li>
<Badge status="success" text="测试通过数量:" />
{item.status4}
</li>
</ul>
</Tooltip>
这样点击卡片就能够调整到UI用例列表的页面了:
查询树和查询列表与之前的模块大同小异;直接CURD就可以了
弹窗可以动态的增加步骤并根据选择操作不同显示/隐藏不同组件:
跳转新页面修改数据后,会自动更新数据
下方选择页面的名称在修改后由“百度搜索主页”自动变为“百度搜索”了:
这里和我们之前实现的页面主要不同点如下:
实现思路如下
我们可以使用antd的ProFormList
组件来实现,文档地址:https://procomponents.ant.design/components/group
参考代码:
<ProFormList
name="steps"
style={{
border: '1px solid #f0f0f0',
borderRadius: 6,
marginTop: 20,
marginBottom: 20,
paddingLeft: 10,
}}
alwaysShowItemLabel
initialValue={caseSteps}
creatorButtonProps={false}
copyIconProps={false}
deleteIconProps={false}
>
{(f, index, { add, remove }) => {
return (
<div style={{ marginTop: 20 }}>
<h3 style={{ fontWeight: 'bold' }}>步骤 {index + 1}</h3>
<ProFormSelect
name="ui_action_id"
label="选择操作"
width="md"
allowClear={false}
fieldProps={{
options: actionData,
showSearch: true,
onChange: (v: any, record: any) => {
const c_step = formRef.current.getFieldValue('steps');
c_step[index].show_type = record.show_type;
formRef.current.setFieldsValue({ steps: c_step });
},
}}
placeholder="请选择操作"
rules={[{ required: true, message: '请选择操作!' }]}
/>
<ProFormDependency
name={['show_type', 'cascader_pages', 'element_id']}
>
{({ show_type, cascader_pages, element_id }) => {
var stepItems: any;
if (show_type == 1 || show_type == 3) {
stepItems = (
<>
<div style={{ display: 'flex' }}>
<ProFormCascader
name="cascader_pages"
label="选择页面"
width="md"
fieldProps={{
changeOnSelect: true,
expandTrigger: 'hover',
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
options: pageTreeData,
onChange: async (page: any) => {
if (page) {
const page_id = page[0];
const c_step =
formRef.current.getFieldValue('steps');
c_step[index].element_id = null; //将选择的步骤值设置为空
formRef.current.setFieldsValue({
steps: c_step,
});
await getUiPagesElements({
page_ids: page_id,
}).then((res) => {
let pageEle = Object.assign(
pageElement,
res.data,
);
console.log('d1', pageEle)
setPageElement(pageEle);
const c_step =
formRef.current.getFieldValue('steps');
c_step[index].cascader_pages = page; //将选择的步骤值设置为空
formRef.current.setFieldsValue({
steps: c_step,
});
});
}
},
}}
rules={[
{ required: true, message: '请选择页面!' },
]}
/>
<span
className={styles.operationStyle}
style={{ marginTop: 5 }}
onClick={() =>
window.open(
`/case/uiCaseOverview/UiPage?project_id=${projectId}`,
)
}
>
维护页面
</span>
</div>
<div style={{ display: 'flex' }}>
<ProFormSelect
name="element_id"
label="选择元素"
width="md"
fieldProps={{
showSearch: true,
options: cascader_pages
? parsePageElement(
pageElement[cascader_pages.slice(-1)],
)
: [],
}}
placeholder="请选择元素"
rules={[
{ required: true, message: '请选择元素!' },
]}
/>
<div style={{ marginTop: 5 }}>
<span
className={styles.operationStyle}
onClick={() => {
if (cascader_pages?.length > 0) {
showEleModal(
REQ_CREATE,
cascader_pages.slice(-1)[0],
index,
);
} else {
message.error('请先选择页面!');
}
}}
>
创建元素
</span>
<span
className={styles.operationStyle}
style={{ marginLeft: 8 }}
onClick={() => {
console.log('ee', element_id);
if (element_id) {
showEleModal(
REQ_UPDATE,
cascader_pages.slice(-1)[0],
index,
{ id: element_id },
);
} else {
message.error('请先选择元素!');
}
}}
>
修改元素
</span>
</div>
</div>
</>
);
}
if (show_type == 2 || show_type == 3) {
return (
<div style={{ marginBottom: 20 }}>
{stepItems}
<ProFormText
allowClear={false}
width="xl"
name="value"
rules={[
{ required: true, message: '请输入值' },
{ type: 'string' },
{ max: 255, message: '最多18个字' },
]}
label="操作数值"
placeholder="请输入操作值"
/>
</div>
);
}
return <div style={{ marginBottom: 20 }}>{stepItems} </div>;
}}
</ProFormDependency>
<PlusCircleOutlined onClick={() => add({}, index + 1)} />
<MinusCircleOutlined
onClick={() => {
if (index > 0) {
remove(index);
}
}}
/>
</div>
);
}}
</ProFormList>
在数据库中我们对每个操作需要的内容都用show_type
这个字段做了区分:
show_type的值对应的含义如下:
show_type = 0 # 只需要操作类型 例如:dirver.close()
show_type = 1 # 需要操作元素,例如:driver.click()
show_type = 2 # 需要操作值,例如:driver.get(url)
show_type = 3 # 即需要元素也需要值,例如:driver.send_keys()
这样我们后续增加操作,也可以直接在数据库中进行添加而不动代码。
然后前端可以使用antd的ProFormDependency
组件来收集每个步骤的show_type
的值,然后根据其不同来返回不同的UI组件即可:
参考代码
<ProFormDependency
name={['show_type', 'cascader_pages', 'element_id']}
>
{({ show_type, cascader_pages, element_id }) => {
var stepItems: any;
if (show_type == 1 || show_type == 3) {
stepItems = (
<>
<div style={{ display: 'flex' }}>
<ProFormCascader
name="cascader_pages"
label="选择页面"
width="md"
fieldProps={{
changeOnSelect: true,
expandTrigger: 'hover',
fieldNames: {
label: 'name',
value: 'id',
children: 'children',
},
options: pageTreeData,
onChange: async (page: any) => {
if (page) {
const page_id = page[0];
const c_step =
formRef.current.getFieldValue('steps');
c_step[index].element_id = null; //将选择的步骤值设置为空
formRef.current.setFieldsValue({
steps: c_step,
});
await getUiPagesElements({
page_ids: page_id,
}).then((res) => {
let pageEle = Object.assign(
pageElement,
res.data,
);
console.log('d1', pageEle)
setPageElement(pageEle);
const c_step =
formRef.current.getFieldValue('steps');
c_step[index].cascader_pages = page; //将选择的步骤值设置为空
formRef.current.setFieldsValue({
steps: c_step,
});
});
}
},
}}
rules={[
{ required: true, message: '请选择页面!' },
]}
/>
<span
className={styles.operationStyle}
style={{ marginTop: 5 }}
onClick={() =>
window.open(
`/case/uiCaseOverview/UiPage?project_id=${projectId}`,
)
}
>
维护页面
</span>
</div>
<div style={{ display: 'flex' }}>
<ProFormSelect
name="element_id"
label="选择元素"
width="md"
fieldProps={{
showSearch: true,
options: cascader_pages
? parsePageElement(
pageElement[cascader_pages.slice(-1)],
)
: [],
}}
placeholder="请选择元素"
rules={[
{ required: true, message: '请选择元素!' },
]}
/>
<div style={{ marginTop: 5 }}>
<span
className={styles.operationStyle}
onClick={() => {
if (cascader_pages?.length > 0) {
showEleModal(
REQ_CREATE,
cascader_pages.slice(-1)[0],
index,
);
} else {
message.error('请先选择页面!');
}
}}
>
创建元素
</span>
<span
className={styles.operationStyle}
style={{ marginLeft: 8 }}
onClick={() => {
console.log('ee', element_id);
if (element_id) {
showEleModal(
REQ_UPDATE,
cascader_pages.slice(-1)[0],
index,
{ id: element_id },
);
} else {
message.error('请先选择元素!');
}
}}
>
修改元素
</span>
</div>
</div>
</>
);
}
if (show_type == 2 || show_type == 3) {
return (
<div style={{ marginBottom: 20 }}>
{stepItems}
<ProFormText
allowClear={false}
width="xl"
name="value"
rules={[
{ required: true, message: '请输入值' },
{ type: 'string' },
{ max: 255, message: '最多18个字' },
]}
label="操作数值"
placeholder="请输入操作值"
/>
</div>
);
}
return <div style={{ marginBottom: 20 }}>{stepItems} </div>;
}}
</ProFormDependency>
实现这个功能我们需要使用到窗口监听事件visibilitychange
,在我们监听到窗口切换后,则自动请求获取元素页面数据的接口:
请求元素页面的方法
const refreshPageTree = useCallback(() => {
if (!window.document.hidden) {
//当页面显示的时候重新请求一次页面树
treeUiPage({ project_id: projectId }).then((res) =>
setPageTree(res.data),
);
}
}, []);
注册监听事件
useEffect(() => {
window.addEventListener('visibilitychange', refreshPageTree);
}, []);
这样在我们每次切换页面后就会调用刷新页面数据的接口进行数据刷新了。
当然,我们处于其他页面时,并不想再让它刷新数据,所以我们可以再关闭弹窗的时候,移除这个监听事件:
modalProps={{
destroyOnClose: true,
maskClosable: false,
onCancel: () => {
window.removeEventListener('visibilitychange', refreshPageTree);
cancel();
},
}}
其他地方的功能都没啥难点了,将填写的数据收集好保存到库中,然后通过执行用例的接口(需自己实现)进行任务的执行即可,小伙伴可以自己动手实现。
selenium有自己的分布式执行方案,也可以远程执行等。但这里存在一个明显的问题(主机和客户机需要再同一网络环境下),在公司还好大家都在局域网,但是想通过平台控制家里的电脑来执行任务就不行了。
为了扩展灵活性并解决这个问题,我采取了心跳机制的方式:服务暴露一个心跳接口,客户端定时轮询请求这个接口,当用户在平台点击执行任务的操作后,服务器会改变执行状态,由于状态的变更,客户端此时请求心跳接口就会拉取任务进行执行了。
拉取到任务后我们就可以根据约定的格式来执行任务了,格式约定可以根据大家的喜好来。也可以参考之前教程:《曲鸟全栈UI自动化教学(八):框架代码讲解和进一步优化》 的执行方式来实现。
至于一些配置(请求密钥、步骤失败后的重试次数、请求的服务器地址等)我采用yaml来管理:
执行UI步骤的dirver代码如下(主要通过反射的方式来选择元素定位方式和要执行的操作),可以参考之前教程:《曲鸟全栈UI自动化教学(八):框架代码讲解和进一步优化》 :
import time
from retrying import retry
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common.by import By
from constant import STEP_ONLY_ACTION, PATH_ELEMENT, STEP_NEED_ELEMENT, STEP_NEED_VALUE, STEP_NEED_ALL, IMAGE_ELEMENT
wait_fixed = 1000
stop_max_attempt_number = 6
class UiDriver():
"""
ui自动化对象
"""
def __init__(self, case_data, settings):
self.driver = None
self.case_data = case_data
self.step_sleep = settings['step_sleep']
globals()['wait_fixed'] = settings['wait_fixed']
globals()['stop_max_attempt_number'] = settings['stop_max_attempt_number']
def run_case(self):
"""
执行UI自动化的入口方法
:return:
"""
print("开始执行UI自动化......")
try:
self.driver = webdriver.Chrome()
except WebDriverException:
input("请下载与谷歌浏览器版本对应的chromedirver!")
cases = self.case_data# case_steps
for step in cases:
time.sleep(self.step_sleep)
self.run_step(step)
return True
@retry(wait_fixed=wait_fixed, stop_max_attempt_number=stop_max_attempt_number)
def run_step(self, step):
"""
执行用例步骤的具体方法
"""
show_type = step['show_type']
print('s', step['ui_action'])
if show_type == STEP_ONLY_ACTION:
getattr(self.driver, step['ui_action'])()
elif show_type == STEP_NEED_ELEMENT:
driver = choose_location_method(step['element_data'], self.driver)
getattr(driver, step['ui_action'])()
elif show_type == STEP_NEED_VALUE:
getattr(self.driver, step['ui_action'])(step['value'])
elif show_type == STEP_NEED_ALL:
driver = choose_location_method(step['element_data'], self.driver)
getattr(driver, step['ui_action'])(step['value'])
def choose_location_method(element, driver):
"""
根据元素类型不同选择不同方式进行定位
:return:
"""
if element['type'] == PATH_ELEMENT:
driver = driver.find_element(getattr(By, element['location_method']), element['path'])
elif element['type'] == IMAGE_ELEMENT:
pass
return driver
当我们完成执行客户端的代码编写后,可以通过pyinstaller
第三方库将其打包为可执行文件,这样别人使用时就不需要考虑环境配置的问题了。谁有,谁就是执行客户端。
这一章完成了UI用例编辑和执行的功能,当然还有很多需要完善的地方,比如增加执行日志,执行完成后的结果判断及变更等。小伙伴们可以按文中的思路和操作方式继续完善。
下一章节将开始接口自动化的教学了。
平台在线演示地址:http://121.43.43.59/ (帐号:admin 密码:123456)
关闭。这个问题需要details or clarity .它目前不接受答案。 想改进这个问题吗? 通过 editing this post 添加细节并澄清问题. 关闭 5 年前。 Improve t
背景 我有一个 Azure Runbook(名为 RunStoredProcedure2),定义如下: param( [parameter(Mandatory=$True)] [string] $Sq
我有一个名为“团队”的表,其中包含“非事件”列,另一个表“事件”,其中包含“时间”列。如果任何团队的“事件”中的最新日期发生在 X 时间之前,如何将“非事件”列更新(为 true)? 我知道这可以通过
下面的问题可能有点令人困惑,但我会尽力以最好的方式解释它。 假设我们为一家制造公司工作。它制造然后用于制造产品的组件。在以下示例中,有 3 个组件和 2 个最终产品。 组件和产品的需求如下: comp
我有代码可以让我在一个范围内选择一个项目: COleVariant vItems = cstrAddr; hr = AutoWrap(
我正在开发一个应用程序,该应用程序有 4 种语言的大约 50 个应用内购买,这给了我很多表单和子表单、框和子框,需要使用 iTunesConnect 的令人痛苦且设计糟糕的表单来填充。 我想知道是否有
我想在 Azure 自动化中使用 powershell 脚本来安排打开/关闭资源。 我想在不创建帐户的情况下执行此操作,因为我们的域强制重置密码。我知道自动化帐户会创建一个证书 - 当使用资源管理器(
我尝试从 azure 自动化 run book power shell 自动检索 azure SQL 数据库中的数据。我发现azure自动化帐户的模块中缺少SQL Server模块。我已经导入了该模块
我正在自动化 Outlook,并且需要控制电子邮件的发件人身份。用户将在 Outlook 中设置两个或多个帐户,我需要能够选择从哪个帐户发送电子邮件。有什么想法吗? 需要 Outlook 2003 及
我尝试从 azure 自动化 run book power shell 自动检索 azure SQL 数据库中的数据。我发现azure自动化帐户的模块中缺少SQL Server模块。我已经导入了该模块
假设我有一个网站,我可以(随时)登录并每隔 x 小时提交数据(单击登录后可见的链接),我将如何自动化此过程? 我构建了一个图形用户界面,它为用户(现在是我,为了我自己的方便)提供了一个包含一些信息的界
我正在开发一个程序,它的任务是我们为它定义一些号码(我们的一些手机号码)并且它应该在 Telegram 中注册它们,然后获取发送到该号码的所有消息。如您所知,在 Telegram 中注册需要提供电话号
关闭。这个问题需要更多focused .它目前不接受答案。 想改进这个问题吗? 更新问题,使其只关注一个问题 editing this post . 关闭 3 年前。 Improve this qu
最近我在处理 CSS Sprite 。一切正常。 我创建了一个 sprite、.css 文件和 html 结构。看起来像 .sprites{ background-image:url
我想为一款名为 Dune 2000 的策略游戏创建一个叠加层。令人讨厌的是,要创建 10 个士兵,每次完成一个都必须单击该图标。没有队列。因此,在不干扰游戏运行方式的情况下,我想听听鼠标移动的声音,当
我是 python 的初学者,我想从自动化开始。以下是我正在尝试执行的任务。 ssh -p 2024 root@10.54.3.32 root@10.54.3.32's password: 我尝试通过
当我将一些 urlencoded Javascript 粘贴到 Firefox 和 Chrome 上的 URL 时,我看到了一些奇怪的事情发生。是否可以使用此技术告诉 Chrome 访问 URL,然后
我需要将大量请求自动提交到基于云的数据库接口(interface) (Intelex)。没有任何方法可以批量提交某些操作,但是提交单个请求所必需的只是让经过身份验证的用户尝试打开 Web 链接。因此,
假设我有一个进程的内存转储。我想对其运行报告,所以基本上我想打开 WinDBG,加载 SOS 并运行一个脚本,该脚本运行一些命令,解析输出,然后基于此运行更多命令。 除了像 SendKeys 这样的
我正在使用 ffmpeg 创建视频剪辑。我想自动化该过程并保存剪辑,而无需手动为要保存的每个文件命名。这是我拥有的代码。它将创建并保存剪辑,但只能使用扩展名和不是名字。 @echo off cd /d
我是一名优秀的程序员,十分优秀!