WinApp自动化测试脚本是使用Appium客户端完成开发的,一些基本的操作方法可参考Appium的API。本节主要介绍WinApp在脚本编写时与移动端App脚本开发上不同的地方。
设置启动程序是在启动参数中添加key为app的参数,值可以是应用程序的ID,也可以是应用程序的完整路径。
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
desired_caps = {}
desired_caps["app"] = "Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"
应用程序ID(AppId)是应用程序用户模型 ID (AppUserModelID),简称 AUMID。在Windows PowerShell中输入命令Get-StartApps便可看到当前用户安装的所有应用名称和 AUMID
PS C:\Users\pc> Get-StartApps
Name AppID
---- -----
Python 3.9 (64-bit) C:\Users\pc\AppData\Local\Programs\Python\Python39\python.exe
Google Chrome Chrome.JOYETIBVDRAA6DX32OHIYXNRNI
uc-devtools com.squirrel.uc-devtools.uc-devtools
SakuraCat com.tidalab.client
微信 D:\Program Files (x86)\Tencent\WeChat\WeChat.exe
Internet Explorer Microsoft.InternetExplorer.Default
闹钟和时钟 Microsoft.WindowsAlarms_8wekyb3d8bbwe!App
计算器 Microsoft.WindowsCalculator_8wekyb3d8bbwe!App
***************************************************
***************** 省略部分内容 *****************
***************************************************
手机连接 Microsoft.YourPhone_8wekyb3d8bbwe!App
Microsoft Store Microsoft.WindowsStore_8wekyb3d8bbwe!App
天气 Microsoft.BingWeather_8wekyb3d8bbwe!App
Solitaire Collection Microsoft.MicrosoftSolitaireCollection_8wekyb3d8bbwe!App
PS C:\Users\pc>
从上内容可以看到已经安装的所有应用AUMID,例如闹钟和时钟应用的AUMID就是Microsoft.WindowsAlarms_8wekyb3d8bbwe!App。
WinApp自动化测试中,在创建Windows应用程序驱动会话时,可以使用的启动参数是有限的
appTopLevelWindow值是窗口句柄,用于设置应用程序顶层窗口,如图
UIA树中就可以知道打开的写字板程序的顶层窗口为【窗格 ‘文档-写字板’】。appTopLevelWindow值是一个16进制,可以通过许多方法获取,
例如
WinAppDriver支持许多方式查找元素。如表7-2列出了支持的定位方式及在Accessibility Insights For Windows工具和Inspect.exe工具中显示的对应元素属性。
//Button[0]
我们可以使用Selenium提供的一些API操作WinApp窗口,例如窗口最大化、设置窗口位置、设置窗口大小、关闭窗口等。
一些Selenium提供的API同样适用于WinApp窗口的方法。
如果在启动参数中app参数的值给定的是一个具体的应用程序ID或完整的路径,那么启动会话后appTopLevelWindow默认就是该应用的根节点。
例如指定的应用程序是写字板,那么启动后会话的appTopLevelWindow默认就是【窗格 ‘文档-写字板’】,后续操作都会将【窗格 ‘文档-写字板’】作为开始节点,例如通过XPATH方式查找元素,就会从该节点开始查找,窗口切换时窗口查找也是查找该节点下的窗口。
UIA树中可以看到,实则还有一个节点【窗格 ‘桌面 1’】,即Root窗口,此节点才是Windows桌面最顶层的窗口,此节点的子节点才是应用程序的根节点。
如果将启动参数app的值设置为‘Root’,那么启动会话后appTopLevelWindow就是Root窗口,表示我们的系统桌面。例如将启动参数app的值设置为‘Root’,然后依次桌面应用程序控制面板和此电脑。
使用Accessibility Insights For Windows工具查看桌面上的元素,如图
所示。从图中可以知道,控制面板和此电脑都有Name属性,因此可以使用NAME定位方式定位元素。
# window_root.py
import time
from appium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
# 添加启动参数
desired_caps = {}
desired_caps['app'] = "Root"
# 客户端连接 Server,启动 Session 会话
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(3)
control_panel_element = driver.find_element(By.NAME, '控制面板')
# 双击桌面上的控制面板
ActionChains(driver).double_click(control_panel_element).perform()
time.sleep(1)
my_computer_element = driver.find_element(By.NAME, '此电脑')
# 双击桌面上的此电脑
ActionChains(driver).double_click(my_computer_element).perform()
time.sleep(1)
# 关闭会话
driver.quit()
运行脚本后,会观察到鼠标先双击桌面上的控制面板并打开,然后双击桌面上的回收站并打开。
注意:此操作是点击桌面上的应用程序,运行脚本时不能有窗口遮挡控制面板和回收站图标,否则会点击到对应位置最上面的窗口上。另一种解决方法是在点击桌面上的应用程序之前,通过脚本先发送快捷键 Win+d,将桌面完全显示出来。
窗口切换与浏览器中的窗口切换相同,如果想操作新窗口,就需要先切换进新窗口。Appium能识别到的窗口是appTopLevelWindow窗口的子窗口,如果是切换appTopLevelWindow窗口的子窗口,则可以通过window_handles、current_window_handle、switch_to.window(window_name)三个API完成切换。
提示:WinAppDriver、Appium、Selenium三者之间的兼容性处理并不是很好,所以句柄的获取会存在很多问题,因此直接使用window_handles获取窗口句柄会有些吃力的。但相信在不久的将来,该问题会得到改善。
因此,如果打开了一个新窗口,而新窗口不在当前会话的appTopLevelWindow节点下,我们就需要根据新窗口句柄启动一个新的会话,开启的新会话就将会话绑定到新窗口上。例如打开写字板程序后,点击插入绘图打开画图窗口,然后关闭画图窗口,最后关闭写字板。
# switch_window.py
import time
import win32gui
from appium import webdriver
from selenium.webdriver.common.by import By
# 添加启动参数
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
# 客户端连接 Server,启动 Session 会话
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(1)
driver.set_window_size(1000, 600)
# 打开绘图窗口
driver.find_element(by=By.NAME, value="绘图").click()
time.sleep(1)
# 获取画图窗口句柄
paint_handle = win32gui.FindWindow(None, '位图图像 在 文档 中 - 画图')
# 配置启动参数,并且启动一个新会话
paint_session = webdriver.Remote(command_executor='http://127.0.0.1:4723',
desired_capabilities={"appTopLevelWindow": hex(paint_handle)})
# 点击画图窗口中的 关闭 按钮
paint_session.find_element(by=By.NAME, value="关闭").click()
# 关闭会话
paint_session.quit()
# 点击写字板窗口中的 关闭 按钮
driver.close()
time.sleep(1)
driver.find_element(by=By.NAME, value="不保存(N)").click()
# 关闭会话
driver.quit()
WinApp自动化上使用代码实现文件上传、文件下载、文件保存或文件另存为都是非常简单的,操作也是相同的,因为都是操作Windows系统应用程序上的窗口,我们只需要根据行为进行元素定位,元素操作即可。下面我们以文件上传为例讲解代码的实现。
由于文件上传的操作步骤都是一致的,因此可以将上传文件场景进行封装,然后调用。例如在写字板中上传图片,当打开上传图片窗口后,先定位文件路径输入框元素并且输入文件路径 ,然后点击打开按钮即可将文件上传到写字板中。实现代码如下:
# upload_file.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 upload_file(driver, file):
# 定位文件路径输入框并输入文件路径
file_input = WebDriverWait(driver, 60, 0.5).until(EC.visibility_of_element_located((By.XPATH, "//Edit[@Name='文件名(N):']")))
file_input.send_keys(file)
# 定位打开按钮并点击
file_open_btn = WebDriverWait(driver, 60, 0.5).until(EC.visibility_of_element_located((By.XPATH, "//Button[@Name='打开(O)']")))
file_open_btn.click()
# 添加启动参数
desired_caps = {}
desired_caps['app'] = r"C:\Program Files\Windows NT\Accessories\wordpad.exe"
# 客户端连接 Server,启动 Session 会话
driver = webdriver.Remote(command_executor='http://127.0.0.1:4723', desired_capabilities=desired_caps)
time.sleep(1)
driver.set_window_size(1000, 600)
time.sleep(1)
# 点击图片,上传文件
(driver.find_elements(by=By.XPATH, value="//Group"))[4].click()
# 文件上传
upload_file(driver, r"D:\ty.png")
# 关闭程序和会话
driver.close()
driver.find_element(by=By.NAME, value="不保存(N)").click()
driver.quit()
注意:运行上面脚本时输入法需要切换到英文,如果是中文输入法,文件路径输入的结果会与预期不一致。
运行上面脚本,可以观察到先启动写字板程序,然后点击了图片图标打开了上传文件窗口,接着文件上传输入框中输入了文件路径,并且点击了打开按钮,此时可以看到写字板中已经成功插入了图片,最后关闭写字板程序,会话关闭。
自动化脚本在输入内容时会受到输入法的影响,例如我们原本想输入内容“D2”,但由于输入法是中文,因此得到的内容将是输入拼音D后列出的第二个汉字,完全与期望相差甚远。
我们在Windows系统上打开一个新程序后,新程序的输入法会是默认语言,例如输入法默认是中文,在Word文件中切换到了英文,当打开记事本后输入法会是默认的中文,当再切回到Word文件时输入法也会跟着切换回英文。
因此我们可以通过设置输入法的默认值达到预期的结果,操作步骤是:Windows10系统下右键点击右下角输入法,然后依次选择【设置】>>【常规】,在默认模式下将【选择输入法默认模式】下拉框选择为预取语言即可,如图标记处,就是将输入法的默认值设置为了英文。
但是项目自动化中我们还是会遇到需要切换输入法的场景。此时我们可以通过输入法切换的快捷键快速实现。例如笔者的默认输入法是中文,中英文切换的快捷键是Shift,改造上面代码中的文件上传函数,在文件路径输入框输入内容前先发送一个Shift快捷键,使输入法切换到英文,然后再输入文件路径。改造后的文件上传函数如下:
def upload_file(driver, file):
# 定位文件路径输入框并输入文件路径
file_input = WebDriverWait(driver, 60, 0.5).until(
EC.presence_of_element_located((By.XPATH, "//Edit[@Name='文件名(N):']")))
# 输入框输入 Shift,由中文切换到英文
file_input.send_keys(Keys.SHIFT)
file_input.send_keys(file)
# 定位打开按钮并点击
file_open_btn = WebDriverWait(driver, 60, 0.5).until(
EC.presence_of_element_located((By.XPATH, "//Button[@Name='打开(O)']")))
file_open_btn.click()
提示:Windows10系统下设置输入法切换的快捷键步骤是:右键点击右下角输入法,然后依次选择【设置】>>【按键】,即可在页面模式切换下找到【中/英文模式切换】、【全/半角切换】等快捷键设置项。
以操作 PC 端的微信为例,聊聊自动化的常见步骤
import time, os
from appium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
from time import sleep
class Auto():
def open_weixin(self, host='localhost', port=4723):
# 打开WinAppDriver服务
# 注意:如果手动开启,则可以注释掉
# os.system(r'start "" /d "C:\Program Files\Windows Application Driver\" "WinAppDriver.exe"')
# 配置信息
# 包含:平台名、系统、应用程序绝对路径
desired_caps = {'platformName': 'Windows', 'deviceName': 'WindowsPC',
'app': r"D:\Program Files (x86)\Tencent\WeChat\WeChat.exe"}
try:
# 连接WinAppDriver服务,打开目标软件
self.driver = webdriver.Remote('http://{}:{}'.format(host, port), desired_caps)
except Exception as e:
raise AssertionError(e)
# 给文件传输助手发送一条信息
def send_msg(self, element_name, msg):
"""
:param element_name:元素name值
:param msg:
:return:
"""
# 通过name属性,找到目标元素
chat_element = self.weixin_driver.find_element_by_name(target_name)
# 点击元素,进入聊天界面
chat_element.click()
# 找到输入框,并输入
self.weixin_driver.find_element_by_name("输入").send_keys(msg)
# 点击右下角的发送,发送消息出去
self.weixin_driver.find_element_by_name("发送(S)").click()
import win32api
import win32con
from appium import webdriver
from selenium.webdriver import ActionChains
# 模拟屏幕滑动
# 1、移动到某个元素区域
ActionChains(self.weixin_driver).move_to_element(
self.weixin_driver.find_element_by_name("element_name")).perform()
# 2、滑动界面
# 比如,向上滚动,模拟滑动
win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, -500)
# 释放资源及关闭服务
def tearDownFunc(self):
print("准备退出")
sleep(2)
# 1、释放资源
self.weixin_driver.quit()
# 2、关闭WinAppDriver应用程序
os.system(' @taskkill /f /im WinAppDriver.exe')