@[TOC](Appium: Windows系统桌面应用自动化测试(四) 辅助工具)
文件批量上传
文件批量上传和文件单个上传原理是相同的,单个上传直接传入文件路径即可,批量上传需要进入批量上传的文件所在目录,然后观察选中多个文件时【文件路径输入框】读取的批量文件写入规则,如图7-12所示,可以看到规则是:“file_name” “file_name”。
我们只需要根据规则将批量文件组合成字符串输入到【文件路径输入框】中,然后点击【插入】即可完成批量上传。
例如打开Word程序,新建一个空白文档,然后依次点击图片>>此设备…,打开插入图片对话框,并在【文件路径输入框】中输入批量上传的文件,最后点击【插入】将多个图片插入到word文档中。代码实现如下:
# upload_files.py
import time
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
def upload_files(driver, file_path, files: list):
# 定位文件路径输入框
file_input = WebDriverWait(driver, 60, 0.5).until(EC.visibility_of_element_located((By.XPATH, "//Edit[@Name='文件名(N):']")))
# 输入框输入 Shift,由中文切换到英文
file_input.send_keys(Keys.SHIFT)
# 先输入批量文件所在的目录,进入该目录下
file_input.send_keys(file_path + Keys.ENTER)
# 生成多个文件上传时的字符串
mult_file_str = ''
for file in files:
mult_file_str += " \"" + file + "\""
# 文件上传
file_input.send_keys(mult_file_str)
file_insert_btn = WebDriverWait(driver, 60, 0.5).until(EC.visibility_of_element_located((By.NAME, "插入(S)")))
file_insert_btn.click()
# 添加启动参数
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE"
# 客户端连接 Server,启动 Session 会话
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
driver.set_window_size(1000, 600)
time.sleep(1)
# 新建空白文档
driver.find_element(by=By.NAME, value="空白文档").click()
time.sleep(1)
# 依次点击 插入 >> 图片 >> 此设备...,打开文件上传窗口
driver.find_element(by=By.NAME, value="插入").click()
driver.find_element(by=By.NAME, value="图片").click()
driver.find_element(by=By.NAME, value="此设备...").click()
# 文件上传
upload_files(driver, r"D:", ['ty.png', 'ty2.png'])
# 关闭程序和会话
driver.close()
driver.find_element(by=By.NAME, value="不保存").click()
driver.quit()
运行上面脚本,可以观察到先启动Word程序,然后点击【空白文档】新建了一个文档,再然后依次点击图片>>此设备…打开了上传文件窗口,上传文件窗口中先是在【文件路径输入框】中输入了"D:“进入到了D盘根目录,接着就在【文件路径输入框】中输入了"ty.png” “ty2.png”,并点击了【插入】按钮,此时可以看到Word文档中已经成功插入了两张图片,如图所示,关闭写字板程序,会话关闭。
获取通知栏信息
Windows系统屏幕右下角(任务栏最右侧)的区域被称为通知区域,通知区域最后一个图标为通知栏图标,点击可打开通知栏,通知栏中可以查看所有被允许的通知信息。Windows也提供了快捷键迅速开启通知栏,例如Win10系统上快捷键就是Win+a。
通知信息如Windows系统更新、截图、安全通知、备份。例如关闭防火墙后通知栏会出现如图所示的消息。
自动化脚本实现获取通知栏信息的操作步骤是设置appTopLevelWindow为Root,定位桌面任意元素并发送快捷键打开通知栏,然后定位通知信息元素,获取到通知元素后便可使用text接口得到文本内容。示例代码如下:
# get_notification_message.py
import time
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
desired_caps = {}
desired_caps['app'] = "Root"
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(1)
# 通过快捷键打开 通知栏
driver.find_element(by=By.NAME, value="任务栏").send_keys(Keys.COMMAND + 'a' + Keys.COMMAND)
time.sleep(1)
# 定位【来自 Windows 安全中心 的通知】通知元素
notification_element = driver.find_element(by=By.NAME, value="来自 Windows 安全中心 的通知")
# 获取【来自 Windows 安全中心 的通知】下所有 Tag 为 Text 的元素文本,并且打印
text_elements = notification_element.find_elements(by=By.TAG_NAME, value="Text")
texts = [text_element.text for text_element in text_elements]
print(texts)
# 通过快捷键关闭 通知栏
driver.find_element(by=By.NAME, value="任务栏").send_keys(Keys.COMMAND + 'a' + Keys.COMMAND)
# 关闭会话
driver.quit()
运行上面脚本后控制台输出内容如下:
['Windows 安全中心', '防火墙和网络保护', '启用 Windows 防火墙', 'Windows 防火墙已关闭。点击或单击以启用。', '22:17']
运行上面脚本后,可以观察到通知栏先开启然后关闭。
Windows系统上,不止通知栏,许多操作都支持快捷键,例如打开文件资源管理器(Win+e)、打开反馈中心(Win+f)、打开剪贴板(Win+v),我们都可以通过此方法对其操作。
滚动条操作
滚动条是常见的一个控件,当内容超过显示区域时便会出现滚动条,通过滑动滚动条可查看所有的内容。
WinAppDriver本身也实现了scroll(xoffset, yoffset)方法操作滚动条,使用方法为
- TouchActions(driver).scroll(0, 100).perform()。
除此之外,我们还可以接着键盘键PageUp和PageDown达到滑动滚动条的目的,下面通过发送键盘键滑动滚动条,选择写字板上的字体。打开字体选择下拉列表后,在循环体中重复接下来的操作,首先查找预期的字体元素,如果出现则点击字体元素,如果不出现则定位任意下拉列表框中的元素发送PageUp或PageDown键,如此循环,直到预期字体元素出现。代码实现如下:
# scrollbar_operation.py
import time
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 添加启动参数
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
driver.set_window_size(1000, 600)
time.sleep(1)
# 字体选择
combox_btn = WebDriverWait(driver, 30, 1).until(
EC.visibility_of_element_located((By.XPATH, "//ComboBox[@Name='字体系列']/Button")))
combox_btn.click()
i = 0
while i < 100:
try:
el = WebDriverWait(driver, 1, 1).until(EC.visibility_of_element_located((By.XPATH, "//ListItem[@Name='幼圆']")))
el.click()
break
except:
i += 1
temp_element = WebDriverWait(driver, 1, 1).until(EC.visibility_of_element_located((By.XPATH, "//List[@Name='字体系列']")))
temp_element.send_keys(Keys.PAGE_DOWN)
time.sleep(1)
# 关闭程序和会话
driver.close()
driver.quit()
运行上面脚本,可以观察到打开字体选择下拉列表后,相隔一定的时间滚动条就下滑一定的距离,当查找的幼圆字体后,点击了幼圆字体,下来列表消失。
其他操作
运行在Windows系统上的应用程序,也有下拉框、单选、多选等控件,这些控件的操作与我们开发其它内容自动化脚本的思路是一致的。都是先熟悉手动操作步骤,然后封装成测试方法,使用时调用封装的方法即可。
例如写字板中选择字体下拉框。封装思路为:先定位打开下拉按钮并点击,使下拉列表显示出来,然后在下来列表中根据字体名查找字体元素,然后点击该字体元素。代码实现如下:
# combox_select.py
import time
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def combox_select(combox: str, value):
combox_btn = WebDriverWait(driver, 30, 1).until(EC.visibility_of_element_located((By.XPATH, f"//ComboBox[@Name='{combox}']/Button")))
combox_btn.click()
value_item = WebDriverWait(driver, 30, 1).until(EC.visibility_of_element_located((By.XPATH, f"//ListItem[@Name='{value}']")))
value_item.click()
# 添加启动参数
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(1)
# 字体选择
combox_select('字体系列', '黑体')
time.sleep(1)
# 关闭程序和会话
driver.close()
driver.quit()
运行上面的脚本,会观察到启动写字板后首先打开了字体选择列表,然后点击黑体字体选择了字体,最后关闭写字板,结束会话。
注意:选择字体时要确保字体在下列列表的可视区域。
对于其他结构相同的下拉框,该方法也同样适用,例如字体大小选择20号字体,使用该方法时就可以写成combox_select(‘字体大小’, 20)。
辅助工具Pywin32
PythonWin32模块是一个非常受欢迎的模块,它提供了从Python访问许多Windows API的功能。自动化测试中使用它可以更方便地实现某些功能,例如窗口截图。PythonWin32模块作为一个成熟的模块。可以辅助自动化测试项目更好地开展,提高工作效率。
Pywin32简介
Pywin32模块是一个第三方模块库,提供了很多访问Windows系统的API,该项目是一个开源项目,在GitHub上可以看到项目源码 Pywin32。
Pywin32的安装和Python的其它第三方库安装方式一样,在命令行工具中输入pip install pywin32即可完成安装。完成后在Python安装路径下~\Lib\site-packages\win32可以看到所有API支撑模块,
下面对部分模块做以介绍:
- win32api:封装Windows Win32 API的模块。
- win32console:Windows控制台函数的接口,用于处理字符模式应用程序。
- win32event:提供win32事件/等待API接口的模块。
- win32file:win32文件API的接口,包括Vista引入的事务性NTFS操作。
- win32gui:提供本机win32 GUI API的接口。
- win32net:封装Windows Network API的模块。
- win32process:提供win32进程和线程API的接口。
- win32security:提供win32安全API的接口。
- win32service:提供Windows NT Service API的接口。
- win32ui:封装Microsoft Foundation类的模块。
其中win32api、win32gui和win32ui是较为重要的三个模块,还有一个消息常量模块win32con。通过这些API,我们可以获取Windows窗口的相关信息,并做简单的操作。
从pywin32所有API支撑模块可以看出,Pywin32库非常强大,提供了丰富的windows系统上的API接口。但是在WinApp自动化中,作为一个辅助工具,我们只用到他的边角料功能就能满足我们的需求。本节将会介绍自动化中使用Pywin32更好地帮助我们完成测试。
常用方法
win32 提供的API非常丰富,下面介绍一些常用的方法:
- win32gui.FindWindow(ClassName, Title):获取当前窗口句柄,句柄是窗口的唯一标识。参数ClassName是窗口的类名,Title是窗口标题。
- win32gui. FindWindowEx(hld, Child, ClassName, Title):获取父窗口下第一个窗口类为ClassName控件的窗口。参数hld是目标窗口的父窗口,Child是目标窗口的子窗口,ClassName是目标窗口的类名,Title是目标窗口的标题。
- win32api.GetCursorPos():获取当前窗口坐标,返回值是tuple类型,例如(721, 550)。
- win32gui.GetWindowRect(handle):获取窗口边框矩形的左上角和右下角坐标,返回值是tuple类型,例如(-25600, -25600, -25441, -25573)。
- win32gui.FindWindow(None, title):根据窗口名查找 Title窗口,返回值是窗口句柄。
- win32gui.GetWindowText(handle):获取窗口标题。
- win32gui.GetClassName(handle):获取窗口类名。
- win32gui.BringWindowToTop(handle):将窗口放在最前面。
- win32gui.SetForegroundWindow(handle):将窗口激活并放在最前面。
- win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0):鼠标左键按下。
- win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0):鼠标左键释放。
- win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0):鼠标右键按下。
- win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0):鼠标右键释放。
- win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, -1):滑动界面,-1表示向下移动一个单位。
- win32api.keybd_event(val, 0, 0, 0):按下键盘某个键。
- win32api.keybd_event(val, 0, win32con.KEYEVENTF_KEYUP, 0):松开键盘的某个键。
获取所有窗口句柄
获取所有窗口句柄我们需要用到win32gui.EnumWindows(callBack,none) 方法,该方法是获取windows所有窗口,无论窗口是否激活。当找到一个窗口就会执行callback一次,传入 当前窗口句柄。将其封装成方法,代码如下:
def get_all_windows():
handle_list = []
win32gui.EnumWindows(lambda handle, param: param.append(handle), handle_list)
return handle_list
# 打印所有窗口 handle
print(get_all_windows())
# 打印窗口 title
print([win32gui.GetWindowText(handle) for handle in get_all_windows()])
键盘按键输入
我们可以通过win32api.keybd_event(bVk, bScan, dwFlags, dwExtraInfo)方法实现键盘事件,因此可以利用它实现键盘按键输入。
keybd_event(bVk, bScan, dwFlags, dwExtraInfo)方法有四个参数,bVk是虚拟键码, bScan是硬件扫描码,一般设置为0即可,dwFlags是函数操作的一个标志位,如果值为KEYEVENTF_EXTENDEDKEY则该键被按下,也可设置为0即可,如果值为KEYEVENTF_KEYUP则该按键被释放,dwExtraInfo是定义与击键相关的附加的32位值,一般设置为0即可。封装方法如下:
def send_keyboard(*args):
for arg in args:
win32api.keybd_event(arg, 0, 0, 0)
for arg in args:
win32api.keybd_event(arg, 0, win32con.KEYEVENTF_KEYUP, 0)
例如 ctrl 键对应的键码是 17,A键对应的键码是65,那么使用封装的方法发送ctrl+A组合键就可写成send_keyboard([17, 65])。
键盘键码请查看附件Ⅰ 键码对照表。
窗口截图
窗口截图的思路是根据窗口句柄获取窗口 DC 和窗口的位置信息及宽和高,然后创建一个新的 DC,再使用新创建的 DC 创建一个兼容设备内容的 DC,最后创建 bitmap,根据 DC 获取图像信息,保存成图片文件。
DC在pywin32中是一个重要概念。windows不允许程序直接访问硬件,所有的操作都需要通过一个设备上下文环境。屏幕上的每个窗口都对应一个DC。DC相当于一个视频缓冲区,对这个缓冲区的操作,会表现在这个缓冲区对应的屏幕窗口上。除了窗口对应的DC外,还可以自己创建DC,然后在创建的DC上面建立数据拷贝到窗口的DC上,就相当于刷新窗口的DC。
示例:打开写字板程序,并对写字板当前窗口截图。根据窗口截图思路可实现截图函数 screenshots_windows。因此实现截图写字板窗口的代码如下:
# win32_screenshots.py
import time
import win32gui, win32ui, win32con
from appium import webdriver
def screenshots_windows(windows_name):
handle = win32gui.FindWindow(None, windows_name) # 获取窗口句柄
win32gui.SetForegroundWindow(handle) # 将窗口放在前台,激活该窗口
hdDC = win32gui.GetWindowDC(handle) # 获取窗口 DC
newhdDC = win32ui.CreateDCFromHandle(hdDC) # 根据句柄创建一个DC
saveDC = newhdDC.CreateCompatibleDC() # 创建一个兼容设备内存的 DC
saveBitmap = win32ui.CreateBitmap() # 创建 bitmap 保存图片
# 获取窗口的位置信息
left, top, right, bottom = win32gui.GetWindowRect(handle)
width = right – left
height = bottom – top
# bitmap 初始化
saveBitmap.CreateCompatibleBitmap(newhdDC, width, height)
saveDC.SelectObject(saveBitmap)
saveDC.BitBlt((0, 0), (width, height), newhdDC, (0, 0), win32con.SRCCOPY)
saveBitmap.SaveBitmapFile(saveDC, windows_name + ".png")
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(3)
screenshots_windows("文档 - 写字板")
# 关闭会话
driver.quit()
注:此截屏函数screenshots_windows来自网络。
运行上面代码后,在当前目录下多了一个“文档 - 写字板.png”文件
脚本录制
脚本录制是通过工具记录用户的操作,并将操作生成脚本,已达到回放的目的。WinAppDriver社区也推出了一个开源的工具WinAppDriver UI Recorder,该工具可以让用户轻松地创建自动化的UI测试。下面我们就来学习WinAppDriver UI Recorder的使用。
-
1.下载WinAppDriver UI Recorder项目,下载地址:https://github.com/Microsoft/WinAppDriver/tree/master/Tools。例如笔者直接下载的是ZIP包,下载并解压后会得到WinAppDriver-master一个文件夹。
-
2.安装Visual Studio 2017及其以上版本的VS工具,例如笔者安装的是VS 2022。
-
3.以管理员身份启动VS,并且打开WinAppDriver UI Recorder项目。启动VS后点击【打开项目或解决方案§】,选择~WinAppDriver-master\Tools\UIRecorder\WinAppDriverUIRecorder.sln文件,
-
4.编译项目。在【解决方法资源管理器】下找到WinAppDriverUIRecorder,右键点击后在弹窗的菜单中选择【生成(U)】开始编译项目,,如果没有控制台没有报错则编译成功。
如果【解决方法资源管理器】没有显示,可在视图菜单下点击使其显示。
-
5.点击VS工具菜单中的【启动】图标:
运行项目,项目启动成功后会打开一个【WAD UIRecorder】窗口,窗口结果(图片来源WinAppDriver UI Recorder官方介绍文档https://blogs.windows.com/windowsdeveloper/2018/06/20/introducing-winappdriver-ui-recorder/)。
-
6.点击【Record】激活录制,然后在正文区域输入一个“s”,如图
从WAD UIRecorder界面上面的面板中可以看到内容区域的xPath定位语法是:
"/Pane[@ClassName=\"#32769\"][@Name=\"桌面 1\"]/Window[@ClassName=\"WordPadClass\"][@Name=\"文档 - 写字板\"]/Document[@ClassName=\"RICHEDIT50W\"][@Name=\"多信息文本窗口\"]"
从下面的C# Code面板中可以看到操作生成的C#代码,代码如下:
// KeyboardInput VirtualKeys=""s"" CapsLock=False NumLock=True ScrollLock=False
Console.WriteLine("KeyboardInput VirtualKeys=\"\"s\"\" CapsLock=False NumLock=True ScrollLock=False");
System.Threading.Thread.Sleep(100);
winElem_LeftClickDocument多信息文本窗口_24_70.SendKeys("s");
使用WinAppDriver UI Recorder工具,用户可以通过更简单、更直观的方法来为WinAppDriver编写自动化脚本。
虽然生成的代码是C#,但是通过该工具可以帮助我们迅速生成测试脚本,对于一些难以用元素识别工具获得元素属性的元素,也可以快速得到定位语法,这也是UI Recorder初始版本最先支持的两个场景。