51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3848|回复: 5
打印 上一主题 下一主题

利用 Selenium 自动化 web 测试

[复制链接]
  • TA的每日心情
    奋斗
    2022-5-8 19:23
  • 签到天数: 137 天

    连续签到: 1 天

    [LV.7]测试师长

    跳转到指定楼层
    1#
    发表于 2011-11-13 15:30:48 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

    [size=1.5em]简介

    Selenium 是一个健壮的工具集合,跨很多平台支持针对基于 web 的应用程序的测试自动化的敏捷开发。它是一个开源的、轻量级的自动化工具,很容易集成到各种项目中,支持多种编程语言,比如 .NET、Perl、Python、Ruby 和 Java™ 编程语言。


    回页首

    [size=1.5em]利用 Selenium 测试 Ajax 应用程序

    Asynchronous JavaScript and XML (Ajax) 是一种用于创建交互式 web 应用程序的 web 开发技术。Ajax 应用程序的一个特征是,不会导致一次重新加载整个页面。相反,浏览器将具有一个对服务器的异步调用以获得数据,并且只刷新当前页面的特定部分。要提高 web 页面的交互性、响应速度和可用性,测试 Ajax 应用程序的过程需要一些改变。

    我们首先刷新 web 页面,然后就是等待,直到异步调用完成。完成之后,可以继续进行验证。此时,出现适当等待时间的问题。

    一种选择是在测试应用程序中简单地暂停一段固定的时间,这在大多数情况下都是可行的。在有些情况下,比如说网络吞吐量很慢时,Ajax 调用在暂停一段特定的时间之后没有完成,会导致测试用例失败。另一方面,如果暂停时间太长,会使得测试慢得不可接受。

    Selenium 提供了更为高效的处理等待的方式。一种可能做法是,使用类 com.thoughtworks.selenium.Wait 来等待一个元素或文本在页面上出现或消失。可以在 until() 函数中定义等待的退出条件,或者扩展 Wait 类来实现等待退出。清单 1 是使用 Wait 类的样例代码。它将在条件满足时停止等待,或者在超出最大等待时间时返回一个超时异常。


    清单 1. 等待元素或文本出现
    Wait wait = new Wait() { public boolean until() { return selenium.isElementPresent(locator); // or selenium.isTextPresent(pattern); } }; wait.wait("", timeoutInMilliseconds);

    另一种选择是使用 Selenium 中的 waitForCondition 函数,一个 JavaScript 代码片段将被作为参数传递给该函数。一旦 Selenium 检测到条件返回为真,它将停止等待。您可以等待一些元素或文本出现或者不出现。JavaScript 可以运行在由Selenium.browser.getCurrentWindow() 函数弹出的应用程序窗口中。清单 2 是检查窗口状态的样例代码。它只工作在 Firefox 中。


    清单 2. 等待窗口就绪的状态
    String script = "var my_window = selenium.browserbot.getCurrentWindow();" script += "var bool;"; script += "var readyState = (my_window.document.readyState);"; script += "if (readyState == 'complete'){"; script += "bool = 'true';"; script += "}"; script += "bool;"; selenium.waitForCondition(script, timeoutInMilliseconds);


    回页首

    [size=1.5em]如何支持 dojo 应用程序

    Dojo 是一个常用的 JavaScript 工具包,用于构造动态 web 界面。使用 Selenium 测试 Dojo 应用程序时的一个关键点是认识 Dojo 小部件和记录它们的操作。作者定义的 Dojo 小部件处于抽象级别。页面运行时,会将 Dojo 小部件转换成基本的 HTML 代码。存在很多由 Dojo 自动生成的 HTML 代码,因此,Dojo 小部件的认识可能与传统 HTML 小部件有些不同。

    Dojo 小部件上执行的操作(包括文本字段、按钮复选框和单选按钮)可能与 HTML 小部件相同。但是,Dojo 在组合框上提供的日期选择器和其他额外的小部件可能需要特定的处理。


    图 1. Dojo 组合框



    使用 Selenium IDE 来记录图 1 中提供的组合框上选中的操作。单击向下箭头,会出现一个下拉列表。选中第三项 Stack(SWG)。记录的脚本提供在图 2 中。


    图 2. Selenium IDE 记录的脚本



    有时,只会由 IDE 生成第二行脚本。在这种情况下,添加单击箭头按钮的操作。对于上面的脚本,如果第一行被重新播放,那么它应该生成下拉列表。但是它不执行任何操作。对于多个 Dojo 小部件,单击并不真正执行单击操作。将 click(locator) 更改为clickAt(locator, coordString) 或者 MouseDown(locator) 和 MouseUp(locator)。

    对于下拉列表,等待时间应该相加。像图 2 中展示的脚本一样,选中项的单击操作将会刚好在单击向下箭头按钮之后执行。它可能会因为下拉列表没有出现而失败。简单地添加一个 pause 命令,或者使用 waitFor 命令等待菜单项元素出现,并继续执行下一个命令。

    修改后的将会自动化 Dojo 组合框上的选择的脚本展示在图 3 中。


    图 3. 修改后的在 Dojo 组合框中进行选择的 IDE 脚本



    RC 代码展示在清单 3 中。


    清单 3. 自动化 Dojo 组合框中选择操作的 RC 代码
    selenium.clickAt("//div[@id='widget_offeringType']/div/div",””); selenium.waitForCondition("selenium.isElementPresent("offeringType_popup2")", "2000"); selenium.clickAt("offeringType_popup2",””);


    图 4. 日期选择器



    对于图 4 中的日期选择器例子,执行的操作可能不会被 IDE 记录。编写如下面清单 4 所示的 RC 代码。


    清单 4. 自动化选择的 RC 代码
    //click on the date field by id you defined; selenium.clickAt("dateBox",""); //wait for the drop down date box by id; selenium.waitForCondition("selenium.isElementPresent("widget_dateBox_dropdown")", \ "2000"); //click previous year 2008; selenium.clickAt("//span[contains(@class,'dijitCalendarPreviousYear')]", ""); //click on the month increase; //previous month would contains ‘dijitCalendarIncrease’. selenium.clickAt("//img[contains(@class,'dijitCalendarIncrease')]",""); //click on the date such as 28 of current month; If you do not specify //the td with the attribute of current month class, it will click \ on the //first 28 of previous month; selenium.click("//td[contains(@class,'dijitCalendarCurrentMonth')]/span[text()='28']");

    如本例所示,Dojo 应用程序不能通过简单的 IDE 记录进行测试。这些脚本有可能不能通过测试。脚本中有一些丢失的操作,或者操作并不真正工作。脚本应该调整成能够在 IDE 和 RC 中顺利地执行。对于复杂的 Dojo 小部件,一种可能的解决方案是使用runScript(String) 函数,因为 Selenium 对 JavaScript 提供很好的支持。清单 5 提供一个 JavaScript 语句来模拟组合框选择。


    清单 5. 运行 JavaScript 语句在组合框上进行选择
    selenium.runScript("dijit.byId("offeringType").setValue("Stack(SWG)");");


    回页首

    [size=1.5em]如何利用 Ant 构建 Selenium 测试

    诸如 Ant 这样的集成工具可以方便地构建 Selenium 测试和顺畅地运行测试用例,无需单独启动 Selenium 服务器。如果 Selenium 测试由 TestNG 驱动,那么定义清单 6 所示 TestNG Ant 任务。清单 6 中假设 classpath 是 TestNG.jar 文件的文件路径。


    清单 6. TestNG Ant 任务
    <taskdef resource="testngtasks" classpath="testng.jar"/>  

    主要的目标是启动服务器、运行测试,然后停止服务器。这些任务按照 bulid.xml 中定义的顺序实现在清单 7 中。


    清单 7. 启动服务器、运行测试用例并停止服务器的 Ant 任务
    <target name="run_test" description="start,run and stop" depends="dist"> <parallel> <antcall target="start-server" /> <sequential> <echo taskname="waitfor" message="Waitforproxy server launch" />  <waitfor maxwait="2" maxwaitunit="minute" checkevery="100"> <http url="http://localhost:4444/selenium-server/driver/?cmd=testComplete" /> </waitfor> <antcall target="runTestNG" /> <antcall target="stop-server" /> </sequential> </parallel>  </target>

    代码更可取的地方是使用 waitfor 任务来测试 Selenium 服务器是否已成功启动,而不是暂停一段固定的时间。如果 URLhttp://localhost:4444/selenium-server/driver/?cmd=testComplete 可用,就意味着 Selenium 已经成功启动。在清单 7 中,它最多等待两分钟,并且每 100 毫秒在本地主机上检查一次 Selenium 服务器,以提供完整的 URL。

    start-server 任务的详细内容定义在清单 8 中。Firefox profile 模板位置和其他参数可以指定在标记 <arg> 中。


    清单 8. 详细的启动服务器的 Ant 任务
    <target name="start-server"> <java jar="lib/selenium-server.jar" fork="true"> <arg line="-firefoxProfileTemplate ${selenium}/profile/" /> </java> </target>

    runTestNG 任务的详细内容定义在清单 9 中。testng 任务的常用属性包括 outputDir 和 xmlfileset。属性 outputDir 用于设置输出报告位置。属性 xmlfileset 用于包含启动 XML 文件。更多选项请参考 TestNG 正式网站。


    清单 9. 运行测试用例的 Ant 任务
    <target name="runTestNG"> <testng outputDir="${testng.report.dir}" sourcedir="${build}" classpathref="run.cp" haltOnfailure="true"> <xmlfileset dir="${build}" includes="testng.xml" /> </testng> </target>

    stop-server 任务的详细内容定义在清单 10 中。


    清单 10. 停止 Selenium 服务器的 Ant 任务
    <target name="stop-server"> <get taskname="selenium-shutdown" src="http://localhost:4444/selenium-server/driver/?cmd=shutDown" ignoreerrors="true" /> <echo taskname="selenium-shutdown" message=" Errors during shutdown are expected" /> </target>

    上面列出了关键任务。将它们组合到您的构建文件,以便利用 Ant 完成良好集成的测试。


    回页首

    [size=1.5em]如何支持测试 HTTPS 网站

    随着互联网日益强调信息安全,越来越多的 web 应用程序在使用 SSL 身份认证。Selenium IDE 默认支持 HTTPS,但是 Selenium RC 不是这样的。Internet Explorer 和 Firefox 中的解决方案各不相同。

    对于 IE,在 setup 目录下的 SSL 支持文件夹中在安装一个证书。如果使用的版本早于 Selenium-RC 1.0 beta 2,请使用 *iehta 运行模式,对于 Selenium-RC 1.0 beta 2 或更晚的版本,使用 *iexplore 运行模式。

    如果测试 HTTPS 网站时出现一个如下所示的安全警告,那么单击 View Certificate 并安装 HTTPS 网站的证书。如果继续弹出警告,那么考虑在 IE 中进行配置。打开 Tool > Internet Options > Advanced,并取消选择 security 分类下的 Warn about invalid site certificatesCheck for publisher's certificate revocation


    图 5. 测试 HTTPS 网站时的安全警告



    [size=1.2em]创建新的 Firefox 配置文件

    对于 Firefox,遵循以下步骤创建定制的配置文件,然后重启服务器:

    • 关闭任何正在运行的 Firefox 实例。
    • 利用配置文件管理器 firefox -ProfileManager 启动 Firefox。
    • 创建一个新的配置文件。出现提示时,为配置文件选择一个目录。将它放在项目文件夹里面。
    • 选择配置文件并运行 Firefox。
    • 利用您将用于测试的自签名证书导航到 HTTPS URL。 出现提示时接受证书。这将在配置文件中创建一个异常。
    • 关闭浏览器。
    • 转到 Firefox 配置文件目录。
    • 删除该目录中除 cert_override.txt 和 cert8.db 文件之外的任何东西。

    默认情况下,Selenium 将在启动 Firefox 的实例时创建一个新的配置文件。当您利用参数 -firefoxProfileTemplate /path/to/profile/dir 启动服务器时,Selenium 将使用一个部分配置文件(带有证书异常)作为创建新配置文件的基础。这将提供证书异常,而避免了使用整个配置文件带来额外的混乱。注意一下在 Selenium RC 1.0 Beta 2 或更晚版本中以 *firefox 模式,以及在 Selenium RC 1.0 Beta 2 之前的版本中以 *chrome 模式启动 Firefox。

    对于运行模式,*chrome 或 *iehta 是较早版本 Selenium RC 中支持 HTTPS 和安全弹出处理的实验模式。自 Selenium-RC 1.0 beta 2 起,它们已经稳定成 *firefox 和 *iexplore 运行模式。请谨慎地根据所使用的 Selenium 版本而使用运行模式。


    回页首

    [size=1.5em]如何高效地认识不带 ID 属性的 web 元素

    使用一个有含义的 ID 或名称是一种高效且方便的定位元素的方式。它也可以改善测试用例的可读性。但是为了每个元素具有一个有含义的、惟一的 ID(尤其是动态元素),Selenium 提供多种策略来认识元素,比如说 Xpath、DOM 和 CSS。

    下面是一个样例,使用三种策略来定位图 6 中提供的动态表格中的一个元素。HTML 代码在清单 11 中。


    图 6. 动态表格样例




    清单 11. 第一个表格列的 HTML 代码
    <table id="test_table" border="1"> <tbody> <tr> <td align="left"> <div class="test_class">Test 1</div> </td> <td align="center" style="vertical-align: top;"> <table id="AUTOGENBOOKMARK_4"> <tbody> <tr> <td align="center" style="vertical-align: top;"> <div> <img alt="supported" src="supported.png"/> </div> </td> <td align="center" style="vertical-align: top;"> <div> <a href="test?name=test1">edit</a> </div> </td> …….

    Xpath 是一种找到不带特定 ID 或名称的元素的简单方式。

    • 如果知道 ID 或名称之外的一个属性,那么直接使用 @attribute=value 定位元素。
    • 如果只知道属性值的一些特定部分,那么使用 contains(attribute, value) 定位元素。
    • 如果元素没有指定的属性,那么利用 Firebug 搜索最近的具有指定属性的父元素,然后使用 Xpath 从这个元素开始定位想要找到的那个元素。

    表 1. 定位元素的 Xpath 表达式
    定位元素Xpath 表达式
    n 行的第一列//table[@id='test_table']//tr[n]/td
    n 行的图像//table[@id='test_table']//tr[n]//img
    ‘Test 1’ 的编辑链接//a[contains(@href,test1)]

    表 1 展示了定位元素的 Xpath 表达式。在 Firebug 的帮助下,Xpath 可以定位元素和复制的元素。在元素没有 ID 和名称时,Selenium IDE 将会采用 Xpath。尽管 Xpath 利用已经录的脚本,有助于保持一致性,但是它高度依赖于 web 页面的结构。这使得测试用例可读性差,增加了维护难度。此外,在 Internet Explorer 7 和 Internet Explorer 8 中运行具有多个复杂 Xpath 表达式的测试用例可能会太慢了。在这种情况下,将 Xpath 更换为 DOM,后者是另一种高效的定位策略。

    DOM 是 Document Object Model(文档对象模型)的缩写。Selenium 允许您利用 JavaScript 遍历 HTML DOM。Java 的灵活性允许在 DOM 表达式中有多个语句,用分号隔开,以及在语句中定义函数。


    表 2. 定位元素的 DOM 表达式
    定位元素DOM 表达式
    n 行的第一列dom=document.getElementByIdx_x('test_table').rows[n-1].cells[0]
    n 行的图像dom=element=document.getElementByIdx_x('test_table').rows[n-1].cells[1]; element.getElementsByTagName_r('IMG')[0]
    ‘Test 1’ 的编辑链接
    dom=function test(){ var array=document.getElementsByTagName_r('a'); var element;for(var i=0; i<array.length; i++) {if(array.attributes.getNamedItem("href").\ value.indexOf('test2')!=-1){element=array;break;}}return element}; test()


    表 2 展示了定位元素的 DOM 表达式。DOM 定位器在 Firefox 和 Internet Explorer 中也有很好的性能。组织 DOM 表达式需要一些 JavaScript 知识。有时,DOM 表达式对于复杂的元素来说太长了,难以看懂(参见表 2 中提到的 Test 1 的编辑链接的表达式)。

    CSS 定位器用于利用 CSS 选择器选择元素。当 HTML 代码具有良好的样式时,可以高效地利用 CSS 定位器。样例表达式展示在表 3 中。


    表 3. 定位元素的 CSS 表达式
    定位元素CSS 表达式
    n 行的第一列css=#test_table .test_class:nth-child(n)
    n 行的图像
    css=#test_table  tr:nth-child(n) > td:nth-child(2) > table td:nth-child(1) > div >  img

    ‘Test 1’ 的编辑链接css=a[href*='test2']

    一般来说,选用熟悉的定位器表达式,并在脚本结构中保持一致。如果有多种表达式可执行,那么使用最高效的方式在 web 页面中定位元素。


    回页首

    [size=1.5em]如何处理弹出窗口

    一般来说,操作都是在由 Selenium 启动的主窗口中执行。如果您想在一个由 window.open 函数生成的新窗口中执行操作,那么将焦点更换到新窗口。在弹出窗口中执行操作之后,焦点返回到主窗口。处理弹出窗口的过程定义在清单 12 中。


    清单 12. 处理弹出窗口的样例代码
    //wait for the popup window with timeout; selenium.waitForPopUp(windowname, timeout); //select the pop up window selenium.selectWindow(popupWindowIdentifier); //perform action on popup window and close the window; .... //return to the main window use 'null' selenium.selectWindow(null);

    windowname 是调用 window.open 函数的第二个参数。上面提到的 popupwindowIdentifier 是一个窗口标识符,可以是窗口 ID、窗口名称、title=the title of the window 或 var=javascript variable。如果弹出窗口的属性未知,但是真的定义了,那么使用getAllWindowIds()、getAllWindowNames() 或 getAttributeFromAllWindows() 函数来检索弹出窗口的属性。

    在最新版的 Selenium RC 1.0.1 中,Selenium 添加了像 selectPopUp(String) 和 deselectPopUp() 这样的方法,它们的功能在以前版本中由 selectWindow(String) 提供。


    清单 13. 处理弹出窗口的弹出函数
    //wait for the popup window with timeout; selenium.waitForPopUp(“”, timeout); //same as selenium.selectWindow selenium.selectPopUp(“”); //perform action on popup window and close the window; .... //same as selenium.selectWindow(null); selenium.deselectPopUp();

    清单 13 展示了处理弹出窗口最简单的方式。您可以保留 waitForPopUp 和 selectPopUp 函数中的第一个参数为空。如果同时弹出多个窗口,请指定窗口属性。


    回页首

    [size=1.5em]如何处理上载/下载文件窗口

    Selenium 使用 JavaScript 来模拟操作。因此,它不支持诸如上载窗口、下载窗口或身份认证窗口之类的浏览器元素。对于非主要窗口,配置浏览器跳过弹出窗口。


    图 7. 安全信息窗口



    跳过图 7 中安全信息窗口的解决方案是打开 Tools > Internet Options > Custom Level。然后启用 Display mixed content

    配置 Internet Explorer 跳过非主要窗口会减少或消除运行测试用例时不必要的处理。但是如果配置了 Firefox,那么将它保存为新的配置文件,并利用定制的配置文件启动服务器。在关于测试 HTTPS 网站的一节中提到了这样做的原因。

    对于上载/下载窗口,最好是处理而不是跳过它们。为了避免 Selenium 的局限性,一种建议是使用 Java 机器人 AutoIt 来处理文件上载和下载问题。AutoIt 被设计来自动化 Window GUI 操作。它可以认识大多数 Window GUI,提供很多 API,并且很容易转换为 .exe 文件,这样的文件可以直接运行或者在 Java 代码中调用。清单 14 演示了处理文件上载的脚本。这些脚本的步骤是:

    • 根据浏览器类型确定上载窗口标题。
    • 激活上载窗口。
    • 将文件路径放入编辑框中。
    • 提交。

    清单 14. 处理上载的 AutoIt 脚本
    ;first make sure the number of arguments passed into the scripts is more than 1 If $CmdLine[0]<2 Then Exit EndIf handleUpload($CmdLine[1],$CmdLine[2]) ;define a function to handle upload Func handleupload($browser, $uploadfile) Dim $title ;declare a variable ;specify the upload window title according to the browser If $browser="IE" Then ; stands for IE; $title="Select file" Else ; stands for Firefox $title="File upload" EndIf if WinWait($title,"",4) Then ;wait for window with title attribute for 4 seconds; WinActivate($title) ;active the window; ControlSetText($title,"","Edit1",$uploadfile) ;put the file path into the textfield ControlClick($title,"","Button2") ;click the OK or Save button Else Return False EndIf EndFunc

    在 Java 代码中,定义一个函数来执行 AutoIt 编写的 .exe 文件,并在单击 browse 之后调用该函数。


    清单 15. 执行 AutoIt 编写的 .exe 文件
    public void handleUpload(String browser, String filepath) { String execute_file = "D:\\scripts\\upload.exe"; String cmd = """ + execute_file + """ + " " + """ + browser + """ + " " + """ + filepath + """; //with arguments try { Process p = Runtime.getRuntime().exec(cmd); p.waitFor(); //wait for the upload.exe to complete } catch (Exception e) { e.printStackTrace(); } }

    清单 16 是处理 Internet Explorer 中下载窗口的 AutoIt 脚本。Internet Explorer 和 Firefox 中的下载脚本各不相同。


    清单 16. 处理 Internet Explorer 中下载的 AutoIt 脚本
    If $CmdLine[0]<1 Then Exit EndIf handleDownload($CmdLine[1]) Func handleDownload($SaveAsFileName) Dim $download_title="File Download" If WinWait($download_title,"",4) Then WinActivate($download_title) Sleep (1000) ControlClick($download_title,"","Button2","") Dim $save_title="Save As" WinWaitActive($save_title,"",4) ControlSetText($save_title,"","Edit1", $saveAsFileName) Sleep(1000) if FileExists ($SaveAsFileName) Then FileDelete($SaveAsFileName) EndIf ControlClick($save_title, "","Button2","") Return TestFileExists($SaveAsFileName) Else Return False EndIf EndFunc

    AutoIt 脚本很容易编写,但是依赖于浏览器类型和版本,因为不同的浏览器和版本中,窗口标题和窗口控件类是不相同的。


    回页首

    [size=1.5em]如何验证警告/确认/提示信息

    对于由 window.alert() 生成的警告对话框,使用 selenium.getAlert() 来检索前一操作期间生成的 JavaScript 警告的消息。如果没有警告,该函数将会失败。得到一个警告与手动单击 OK 的结果相同。

    对于由 window.confirmation() 生成的确认对话框,使用 selenium.getConfirmation() 来检索前一操作期间生成的 JavaScript 确认对话框的消息。默认情况下,该函数会返回 true,与手动单击 OK 的结果相同。这可以通过优先执行chooseCancelOnNextConfirmation 命令来改变。

    对于由 window.prompt() 生成的提示对话框,使用 selenium.getPromt() 来检索前一操作期间生成的 JavaScript 问题提示对话框的消息。提示的成功处理需要优先执行 answerOnNextPrompt 命令。

    JavaScript 警告在 Selenium 中不会弹出为可见的对话框。处理这些弹出对话框失败会导致异常,指出没有未预料到的警告。这会让测试用例失败。


    [size=1.5em]参考资料

    学习

    获得产品和技术

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    该用户从未签到

    2#
    发表于 2011-12-1 10:33:21 | 只看该作者
    好长...
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    3#
    发表于 2011-12-1 16:48:16 | 只看该作者
    表示没看到表!?
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    4#
    发表于 2011-12-1 16:49:32 | 只看该作者
    表示没看到表!?
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    5#
    发表于 2011-12-1 16:49:42 | 只看该作者
    表示没看到表!?
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    6#
    发表于 2011-12-12 15:03:12 | 只看该作者
    看不到表
    回复 支持 反对

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-24 05:38 , Processed in 0.096126 second(s), 27 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表