RFT 抓取对象

Rational Functional Tester (RFT) 作为 IBM 自己设计研发的自动化测试工具,适用范围非常广泛,仅在 IBM 公司内部,其使用范围已覆盖 WPLC 的各大 Web 产品:如 WepSphere Portal,Lotus Quickr,Lotus Connection,Lotus iNotes,Lotus Mashup Center 等,并且深入到测试的各个阶段:如 UAT(User Acceptance Test,用户可接受度测试),BVT(Build Verification Test,小版本测试),FVT(Functional Verification Test,功能测试),Migration(迁移测试),Regression(回归测试),甚至 GVT(Globalization Verification Test,国际化支持测试)。测试环境的复杂化、测试用例的多样化、测试产品的开发特性(如对 Accessibility 的完备支持)等都对自动化程序的开发者带来困扰和挑战。本文从 3 个常见的问题入手,进行具体分析,并给出解决方案。

多浏览器窗口

在某些测试用例中,需要同时处理 2 个甚至多个浏览器窗口。比如浏览器窗口 A 上登录系统的用户 user1 和浏览器窗口 B 上登录系统的用户 user2 相互聊天,或者 user1 将创建的内容 publish 到另外一个系统上,同时 user1 要在另一个浏览器窗口中对发布到目标系统的内容进行验证,类似的操作要重复多次。这种情况下,如果脚本每次只能针对一个浏览器进行处理,程序执行的效率将 会非常低下。问题的关键在于,对于多个浏览器窗口,脚本如何精确区分它们。这里,以常见的 2 个浏览器窗口为例,介绍 3 种区分和处理方法:

利用 ProcessTestObject 来进行区分

ProcessTestObject 是 RFT 中一个比较特殊且有用的类,它是对一个进程对象的封装。从图 1 所以的结构层次可以看出,它和 RootTestObject,DomainTestObject 处于同一个层次,直接实现了 TestObject 接口。而一个 ProcessTestObject 对象则是对某个具体进程的抽象,它含有的唯一属性就是进程 ID,并且只要该进程存在,该对象就有效(该对象的 exists () 和 isActive () 方法指示进程是否结束)。


图 1. ProcessTestObject 继承关系
图 1. ProcessTestObject 继承关系

对于 Browser 来说,每次启动一个浏览器实例,在系统中就产生一个进程,因此可以利用 ProcessTestObject 来区分不同的浏览器实例。那么如何得到每个浏览器的 ProcessTestObject 对象呢?注意到 RationalTestObject 有一个静态方法:startBrowser(String browsername, String url),它返回一个 ProcessTestObject 对象。因此脚本需要在每次启动浏览器的时候,保存相应的 ProcessTestObject 对象,脚本就可以用它来区分和查找不同浏览器实例中的测试对象。示例代码如下:


清单 1. 根据进程对象来区分不同的 browser 实例

/**
 * 根据进程对象来区分不同的 browser 实例,进而在该 browser 中查找特定测试对象
 */
public TestObject getTestObject(ProcessTestObject process, String property1, 
    String value1, String property2, String value2){
BrowserTestObject bBrowser=null;
TestObject[] browsers=getRootTestObject().find(atProperty(".class","Html.HtmlBrowser"));
if(browsers.length==0) return null;
for(int i=0; i<browsers.length; i++){
if(browsers[i].getProcess().equals(process)){
bBrowser= (BrowserTestObject)browsers[i];
break;
}
}
if(null == bBrowser) return null;
TestObject[] objects=bBrowser.find(atDescendant(property1,value1,property2,value2));
if(objects.length>0) return objects[0];
return null;
}

 

利用 ProcessTestObject 对象来区分浏览器窗口仅限于多个独立 IE 窗口之间,以及多个 IE 窗口和一个 Firefox 窗口的情况。因为:通过 1)宿主 IE 窗口和寄生 IE 窗口(在打开一个链接的同时,按住 Ctrl 键而产生的 IE 窗口)隶属于同一个进程;2)所有的 Firefox 窗口都隶属于同一个进程。

利用 RFT 对 HTML Domain 的支持来进行区分

目前 RFT 对浏览器的支持只包括 IE 和 Firefox,它们同属于 HTML domain。图 2 显示了当前 RFT 所支持的部分 Domain 以及每个 Domain 针对不同应用程序的不同实现。


图 2. RFT 实现的 Domain(部分)
图 2. RFT 实现的 Domain(部分)

由图 2 可知,RFT 对两种浏览器进行了不同的实现。那么是否存在一种属性可以在 Domain 实现层次对 IE 和 Firefox 进行区分呢?答案是肯定的。这个属性就是:Domain 的 ImplementationName。对 IE,该属性值为 MS Internet Explorer,而对 Firefox,该属性值为 Firefox。据此,脚本可以在 Domain 实现层次对 IE 和 Firefox 窗口进行区分。示例代码如下:


清单 2. 根据 RFT 对 2 种 browser 在 Domain 层次的不同实现名称来区分不同的 browser 实例

/**
 * 根据 RFT 对 2 种 browser 在 Domain 层次的不同实现名称来区分不同的 browser 实例,进而在该 browser 中查找特定测试对象。
 */
public TestObject getTestObject(boolean bIEOrFF, String property1, 
    String value1, String property2, String value2){
String browserImplementName=bIEOrFF?"MS Internet Explorer":"Firefox";
DomainTestObject[] domains=getDomains();
TestObject root=null;
for(int i=0; i<domains.length; i++){
Object domainName=domains[i].getName();
Object domainImplName=domains[i].getImplementationName();
if(domainName.equals("Html") && domainImplName.equals(browserImplementName)){
TestObject[]topBrowser=domains[i].getTopObjects();
root= (topBrowser.length>0)? topBrowser[0]: null;
break;
}
}
if(null == root) return null;
TestObject[] objects=root.find(atDescendant(property1,value1,property2,value2));
if(objects.length>0) return objects[0];
return null;
}

 

该方法只适用于区别一个 IE 窗口和一个 Firefox 窗口的情况。对于该种情况,另外一种可行的方法是根据 browser 的名称来动态查找。示例代码如下:


清单 3. 根据 browser 名称来区分不同的 browser 实例

/**
 * 根据 browser 名称来区分不同的 browser 实例,进而在该 browser 中查找特定测试对象
 */
public TestObject getTestObject(boolean bIEOrFF, String property1, 
    String value1, String property2, String value2){
String browserName=bIEOrFF?"MS Internet Explorer":"Firefox";
TestObject[] browsers=getRootTestObject().find(atDescendant(".class","Html.HtmlBrowser",
    ".browserName",browserName));
if(browsers.length==0) return null;
TestObject[] objects=browsers[0].find(atDescendant(property1,value1,property2,value2));
if(objects.length>0) return objects[0];
return null;
}

 

利用 browser 的特殊属性进行区分

如前 2 中方案所述,可以针对一定情况下的浏览器窗口组合进行区别,不能处理所有情况,如 2 个或多个 Firefox 窗口的情况。对于这种情况,可以利用 browser 的一些特殊属性来进行区分,如浏览器窗口的 .window 属性,任何一个窗口都有唯一的 window ID 来标识它,因此对于 IE 和 Firefox 的任意组合,均可以处理。示例代码如下:


清单 4. 根据 browser 窗口的 window ID 来区分不同的 browser 实例

/**
 * 根据 browser 窗口的 window ID 来区分不同的 browser 实例,进而在该 browser 中查找特定测试对象
 */
public TestObject getBrowser_3(int windowID, String property1, 
String value1, String property2, String value2 ){
TestObject[] browsers=getRootTestObject().find(atDescendant(".class","Html.HtmlBrowser",
    ".window",String.valueOf(windowID)));
if(browsers.length==0) return null;
TestObject[] objects=browsers[0].find(atDescendant(property1,value1,property2,value2));
if(objects.length>0) return objects[0];
return null;
}

 

对于临时出现的 Browser 窗口,还可以利用 BrowserTestObject 的 .documentName,DocumentTestObject 的 .title 和 .url 等来区分:在新窗口出现之前,保留原来窗口的相应属性值,在新窗口出现之后进行过滤和计算。

相同的 test object 对象识别

针对 Web 应用程序的测试中,经常会遇到在同一个页面上存在(指显式存在,即测试人员能肉眼看到。相同属性的隐藏对象不在此列)“长相“完全一样的测试对象,如果识 别不准确,会导致脚本误操作,偏离正常测试逻辑。这种相同对象需要特别留意,通常情况下,脚本可以首先获取每个对象的可区分的父对象,进而在该父对象内精 确查找出它。然而还存在父对象也相同的情况,这里列举出 3 个实例,并通过对具体实例 HTML 源代码的分析,解释不同的方案和特点及其适用范围。

index 的计算

如果有多个相同的测试对象位于同一个父对象内,且所处的层次 / 深度相同,可利用 index 的计算来区别每一个测试对象。通常 index 标识了该对象在整个页面结构中的出现次序。如图 3 示例:


图 3. 相同的测试对象之 index 计算
图 3. 相同的测试对象之 index 计算

页面上存在 3 个 CheckBox(图 3 的源代码中只列出后 2 个),它们的属性完全相同,并且他们位于同一个 Table 元素的不同行上,所不同的仅仅是 Table 元素的某一列。因此可以首先根据“Remote portlet”列的值“Remote”计算出该行 CheckBox 的 index 值,也即目标 CheckBox 是当前 parent 对象(Table)中的第几个 CheckBox,然后利用 find() 方法查找出 parent 对象中所有的 CheckBox 对象,直接返回第 index 个 Check Box 即可。示例代码如下:


清单 5. 计算出目标对象的 index

/**
 * 在给定的父对象 parent 中,计算出目标对象的 index,进而通过动态查找方法直接获取目标测试对象。
*/
public WRadioButton getRadioButton(WTable parent, String column1, String column3){
if(null == parent) return null;
int findIndex=-1;
for(int i=0; i<parent.getRowCount(); i++){
if(!parent.getCellContents(i, 0).matches(column1)) continue;
if(!parent.getCellContents(i, 3).matches(column3)) continue;
findIndex=i;
break;
}
if(-1 == findIndex) return null;
TestObject[] checkboxes=parent.find(atDescendant(".class","Html.INPUT.checkbox",
    ".className","wpsCompactCheckBox"));
if(findIndex>checkboxes.length-1) return null;
return new WRadioButton(checkboxes[findIndex]);
}

 

键盘操作

键盘操作通常比多次利用 find() 查找对象要快速有效,但是需要先找到一个合适的锚点。如图 4 所示,存在两个属性一样的“Portlets”链接,除了通过它们的父对象进行区别,使用键盘也是一种选择:在右上角的翻页图标上右击,然后用 “Escape”键取消掉弹出的右键菜单,然后连续 6 次 Tab 键就可以将当前焦点移动到右边的“Portlets”链接上,最后通过回车键“Enter”,即可达到点击“Portlets”链接的目的。


图 4. 相同的测试对象之键盘操作
图 4. 相同的测试对象之键盘操作

示例代码如下:


清单 6. 间接操作目标对象

/**
 * 通过在参考锚点对象(imageGo)上的键盘操作,将输入焦点移动至目标对象,然后利用回车键达到间接操作目标对象的目的。
*/
public void clickPortletByKeyStroke(){
TestObject[] nextImages=getRootTestObject().find(atDescendant(".title", "Go",
".class", "Html.INPUT.image"));
if(nextImages.length==0) return;
//ESC to disaapear context menu,6 tabs to move focus to "Portlets" link,and Enter to click
String keyStroke = "{ESC}{Tab 6}{ENTER}";
GuiTestObject imageGo=(GuiTestObject)nextImages[0];
//right click "Go" image
imageGo.click(RationalTestScriptConstants.RIGHT);
//type keystroke in current window to click "Portlet" link
IWindow topWindow=getScreen().getActiveWindow();
topWindow.activate();
topWindow.inputKeys(keyStroke);
}

 

针对 Web 控件的键盘操作依赖于 Web 应用的实现,如果锚点控件的右键功能被禁止,那么该方法将无效。同时,还可以考虑左键点击,此时要求该 Web 控件接受左击事件时只触发焦点移动,而没有其他行为。比如:<span id=”W7d647d64” class=”txt”>Login</span>,左击 Login 文本时,将输入焦点移动至 Login 所在的 SPAN 元素上,再通过键盘操作就可以将输入焦点切换至目标对象。而针对某些修饰性的文本,如 <font class=\”wizard-text\”> What’s New</font>, What’s New 只是作为修饰性的文本存在,其根本不能接受任何键盘事件和输入焦点。因此利用键盘需要事先明确是否锚点控件可利用键盘操作来切换焦点。

利用特殊 HTML tag 的特性

某些 Html Tag 的特性也可以辅助脚本来区别和操作相同的测试对象,但这依赖于 Web 应用的开发风格和规范,以及对产品 Accessibility 的支持程度。

对于图 5 中的 4 个 Radio Button,它们并列位于某一个父对象的同一层次,可以利用 index 计算的方式获取。同时,注意到每一个 Radio Button 都和一个 Label 元素通过 for 属性绑定起来 (For 属性的值对应它所服务元素 -Radio Button 的 id 属性值 ),因此,直接点击 Label 元素就可以达到选择相应的 Radio Button 的目的。这可以使脚本省去复杂的 index 计算。

Label 对象还有一个同样重要的属性:ACCESSKEY,它指定了针对服务元素的热键。这同样可以使脚本免去动态查找对象的麻烦,直接使用热键完成对目标控件的操作。


图 5. 相同的测试对象之 Label 特性
图 5. 相同的测试对象之 Label 特性

示例代码如下:


清单 7. 通过点击 Label操作目标对象

/**
 * 利用 HTML Label 的 Binding 特性,通过点击 Label 达到操作目标操作对象的目的。
*/
public void selectFirstCheckBox(){
TestObject[] labels=getRootTestObject().find(atDescendant(".class","Html.LABEL",
".text","First child"));
if(labels.length==0) return;
GuiTestObject firstcb=(GuiTestObject)labels[0];
firstcb.click();
}

 

Label 的上述特性可以利用至任何拥有 id 属性的 HTML 标签。

Windows 弹出窗口处理

在自动化脚本的运行过程中,弹出窗口的出现具有致命威胁,它导致脚本停滞不前,通常需要人为干预才可以继续;如果没有人为干预,脚本会耗费很长时间,并且以失败结束。

这里,弹出窗口可以从两个方面来进行处理:1)意外窗口或者 sidebar 窗口,这类窗口的出现多可以通过系统配置,尤其是对 browser 的配置,来阻止其弹出,避免其干扰脚本的正常执行;2)通过脚本中加入特殊的处理代码进行处理,例如,测试用例中要求下载某文件时,必须对弹出的下载对话 框进行处理。这类弹出窗口之所以成为问题,是因为 RFT 对某些平台的控件识别能力不足导致的。

  1. 意外窗口的避免

这里仅举 3 个例子,并给出相应的系统配置加以避免:

示例 A.通常在 Firefox 崩溃之后,下次启动时会出现图 6 所示对话框窗口:


图 6. Firefox 的 Session 恢复
图 6. Firefox 的 Session 恢复

解决方法:在 Firefox 的地址栏中,输入 about:config,打开配置界面,然后新建 Boolean 类型的如下参数:browser.sessionstore.enabled,并赋值 false;

示例 B.在访问某些采用了 SSL 协议的 Web 应用程序时,可能会遇到图 7 所示对话框。脚本要继续执行,必须选择”Yes”。:


图 7. SSL 显示不安全内容提示框

解决方法:打开 IE 浏览器,进入“Tools”=>“Internet Options”=>“Custom Level…”,使能“Display mixed content”,如下图 8 所示:


图 8. 禁止显示不安全内容提示框
图 8. 禁止显示不安全内容提示框

示例 C.如果需要从浏览器上下载某些文件,可能会出现图 9 所示提示栏,RFT 无法识别它。


图 9. 下载 side info 提示栏
图 9. 下载 side info 提示栏

解决方法:打开 IE 浏览器,进入“Tools”=>“Internet Options”=>“Custom Level…”,使能“Automatic prompting for file downloads”和“File download”,如图 10 所示:


图 10. 允许下载对话框直接出现
图 10. 允许下载对话框直接出现

这种通过系统配置来限制意外窗口的弹出是自动化脚本开发人员的首选方案,应该从脚本运行的标准环境配置的角度来看待和重视它。否则,即使脚本可以处理,它也需要考虑各种可能出现的窗口,以致于引入不必要的复杂额外逻辑,代码臃肿且效率低下。

特殊代码处理弹出对话框

对于测试用例中必须要处理的系统弹出窗口,处理方式可以有:1)利用 RFT 提供的 IWindow 接口来处理简单的窗口;2)引入辅助工具来识别复杂窗口控件,然后把由辅助工具开发的程序整合进 RFT 来处理弹出窗口。以下分别就图 11 中下载文件的实例用 2 种方式进行详细说明和比较。


图 11. Windows 下载对话框
图 11. Windows 下载对话框

方案 A. IWindow 接口处理

IWindow 是 RFT 对 Window 平台的控件的一种抽象描述,所有的控件,不论是对话框,按钮,还是输入框,都是对 IWindow 接口的一种具体实现。这种统一的接口简化了对象识别,易于操作。

IWindow 利用两个核心的属性来表示一个具体的窗口控件:Text 属性和 ClassName 属性。如图 12 所示,(&Save, Button) 中,“&Save”是 save 按钮的 text 属性,而“Button“表示它是一个按钮:


图 12. IWindow 接口对 Windows 对话框上的对象识别
图 12. IWindow 接口对 Windows 对话框上的对象识别

在 RFT 中,可以通过 Object Map 这一图形化工具来直接获取上图中所列控件的 Text 属性和 ClassName 属性。除此之外,还可以通过如下示例代码来打印给定窗口中所有 windows 控件的识别属性,进而筛选出脚本真正需要的。

示例代码:printWindowsProperties() 打印窗口 "Save As" 中所有控件的属性:


清单 8. 打印窗口 "Save As" 中所有控件的属性

/**
 * 该方法打印出”Save As”对话框中所有控件的 Text 属性和 ClassName 属性。
*/
public void printWindowsProperties() {
// 由于“Save As”窗口唯一,只用 Text 属性即可找到它,因此设置 //ClassName 属性为 null;
IWindow currentWindow = getWindowByProperties(null, "Save As", null);
if (null == currentWindow) return;
IWindow[] children = currentWindow.getChildren();
for (int i = 0; i < children.length; i++) {
String textProperty = children[i].getText();
 String classProperty = children[i].getWindowClassName();
System.out.println("Child[" + i + "]'s properties=("
+ textProperty + "," + classProperty + ")");
}
}

 

上述代码中,getWindowByProperties() 获取给定属性的 IWindow 控件,实现代码如下:


清单 9. 获取给定属性的 IWindow 控件

/**
 * 在给定的 parent 窗口中,根据制定的 Text 属性和 ClassName 属性查找控件
*/
public IWindow getWindowByProperties(IWindow parent, String textProperty,
String classNameProperty) {
IWindow[] windows = null;
if (null == parent) {//parent 为 null,从当前所有窗口中查找
windows = getTopWindows();
} else {// 从给定的 parent 窗口中查找给定窗口
windows = parent.getChildren();
}
for (int i = 0; i < windows.length; i++) {
String _text = windows[i].getText(), _class = windows[i]
.getWindowClassName();
boolean find = false;
if (null != textProperty) {// 如果 Text 属性非空
find = _text.matches(textProperty);
if (!find)continue;
}
if (null != classNameProperty) {// 如果 Class Name 属性非空
find = _class.matches(classNameProperty);
}
if (find) {
return windows[i];
}
}
return null;
}

 

因此,针对图 12 中“Save As”对话框,可以用如下代码处理:


清单 10. 针对图 12 中“Save As”对话框

/**
 * 该方法首先获取”Save As”对话框,激活它,然后找到 File name 输入框,输入文件路径后,
 * 找到 Save 按钮,并点击之。等待一段时间后,验证文件是否下载成功。
*/
public boolean downloadFile(String filepath){
IWindow saveAsWin=getWindowByProperties(null, "Save As", null);
if(null == saveAsWin) return false;
saveAsWin.activate();
// 通过 ClassName 属性获取文件输入控件对象
IWindow filenameWin=getWindowByProperties(saveAsWin,null, "ComboBoxEx32");
if(null == filenameWin) return false;
// 利用键盘操作输入文件保存路径
saveAsWin.inputKeys(filepath);
// 利用 Text 和 ClassName 属性找出 Save 按钮,并点击
IWindow saveBtn=getWindowByProperties(saveAsWin,"&Save","Button");
if(null == saveBtn) return false;
saveBtn.click();
sleep(5);
// 检查文件是否下载成功
return new File(filepath).exists();
}

 

利用 IWindow 接口处理 Windows 窗口,原理简单,且保证 100% 纯 Java 代码,同时缺点也是很明显的:1)需要较多的编程;2)IWindow 接口的功能尚有限。对于上述输入文件路径的输入框,IWindow 没有提供 setText 方法,只能利用键盘操作,并且由于 inputKeys() 方法继承自 ITopWindow,因此它只对 Top Level 的窗口有效,不能使用 filenameWin inputKeys(filepath) 来输入文件路径。另外,一旦窗口中含有多个输入框,还需要额外的逻辑先去判断当前窗口的焦点位置(并且,IWindow 的 hasFocus() 方法并不有效)。

方案 B. 引入辅助工具进行处理

针对 IWindow 处理 Windows 控件的缺点,第二种方案是引入一些辅助工具,大部分情况下会起到事半功倍的效果,推荐使用的工具包括:AutoIt(免费使用,但是需要律师的审 核),Rational Robot(IBM 自己的产品 )。这 2 款工具都非常胜任对 Windows 控件的识别和支持。

AutoIt 本身就是设计用来在 Windows GUI 中进行自动化操作的工具,对于绝大多数的 Windows 控件都可以自动识别,且提供很多的 API 库供用户使用,且开发出来的程序可以方便地转换为 .exe 可执行文件。本文即用它来演示如何操作图 12 中的文件下载对话框。

AutoIt 对 Windows 控件的识别非常直观,如图 13 所示,识别信息包含“Basic Window Info”(顶层窗口对象信息)和“Basic Control Info”(顶层窗口内子控件对象信息)两部分。对于输入框而言,它的 Class 是 Edit,每个输入框都有一个唯一的 instance 标识。Class 和 Instance 组成 controlID 就可以在窗口中确定唯一的输入框。


图 13. AutoIt 对 Windows 对话框的识别
图 13. AutoIt 对 Windows 对话框的识别

而 AutoIt 提供的丰富的库函数可以方便的处理 Windows 控件。比如:向输入框中填入数据:ControlSetText ( "title", "text", controlID, "new text"),前 2 个参数唯一标识顶层窗口的标题和包含的文本串,controlID 表示该顶层窗口内的目标输入控件,new text 为输入内容。因此处理图 12 中对话框的 AutoIt 示例源代码如下(表示注释行):


清单 11. 处理图 12 中对话框的 AutoIt 示例源代码

If $CmdLine[0]<1 Then Exit EndIf
handleDownload($CmdLine[1])
; 方法 handleDownload()接受文件路径作为参数,完成下载操作。
Function handleDownload($SaveAsFileName)
$title=”Save As”
If WinExists($title) Then
WinActivate($title)
ControlSetText($title,””,”Edit1”, $saveAsFileName)
ControlClick($title, “”,”&Save”)
Return FileExists($SaveAsFileName)
Else
Return False
EndIf
EndFunc

 

上述代码接受一个输入参数,由调用者指定文件的存放位置。经过 AutoIt 编译器编译之后,可产生可执行文件 handleDownload.exe。脚本开发者可以定义一个 java 方法,将之封装起来,作为公用的方法在以后程序中使用。封装的示例代码如下:


清单 12. 定义一个 java 方法

public void handleDownload(String filepath){
String sToolName="x:\\handleDownload.exe";
String sCMD="\""+sToolName+"\""+" "+"\""+filepath+"\""; // 带输入参数的命令
try{
java.lang.Process p=Runtime.getRuntime().exe(sCMD);
p.waitFor();// 等待 handleDownload.exe 执行结束
}catch(Exception e){
e.printStackTrace();
}
}

 

Rational Robot 具有类似强大的 windows 控件识别和处理功能,且是 ibm 自己开发的产品,非常适合做此类整合,在此不再赘述。

引入辅助工具后,还会涉及到 Java 方法如何获取 .exe 执行的结果,如何保证 .exe 进程可控等问题。

引入辅助工具后,编码的效率得到很大提高。表现在:1)辅助工具提供对 Windows 控件更强大的支持,能识别和操作绝大多数的 Windows 控件;2)编程的逻辑清晰简洁。

总结

本文通过笔者在使用 RFT 对 Web 应用程序进行自动化测试过程中所遇到的常见的 3 类问题进行了细致的梳理和总结:1)多浏览器窗口的区别方法;2)相同测试对象的识别;3)测试中过程中弹出窗口的常用处理方法。希望可以给做自动化测试 的同仁们提供一定的借鉴和参考作用。

你可能感兴趣的:(对象)