自动化工具 selenium: 死磕页面跳转,主页-登录页-主页

cheryl.cai · 发布于 2017年10月11日 · 最后由 fudax 回复于 2017年10月12日 · 875 次阅读
本帖已被设为精华帖!

最近在尝试将工作中常用到的一些配置操作从手工替换成自动化脚本,为实现这个目标我尝试了selenium IDE和selenium webdriver。在这篇文章中想要分享的是登录页面跳转这个简单步骤的一些操作和思考。目标操作步骤如下图所示:

使用selenium IDE的实现

鉴于公认的selenium IDE上手容易,我首先尝试在selenium IDE里录制整个过程。下图是录制出来的结果,只有4个command,打开主页网址,在跳转到的登录页输入用户名、密码信息,提交信息跳转显示主页。在这个过程中,以下几个问题值得思考。

  • 等待页面加载:selenium IDE 可以控制回放脚本的执行速度,如果执行速度过快,会有一些元素还没有加载出来就被下一步使用,这会导致下一步出现“element not found”错误。但open命令和clickAndWait命令,都是打开加载新的页面,却基本不会出现这种问题。回放速度的快慢并不会影响这两个命令。可以看一下这两个命令的说明:

    • open: waits for the page to load before proceeding, ie. the "AndWait" suffix is implicit.
    • clickAndWait: If the click action causes a new page to load (like a link usually does), call waitForPageToLoad.
    • 与此类似的还有waitForPageToLoad、waitForText、waitForElementPresent等命令均可以用来等待页面加载、预防出现因来不及加载完全而出现的问题。
  • 参数变量化:直接录制的结果中,用户名、密码是直接记录在command对应的value里的。如果要复用该test case,用户名密码可能会改变,这时候就需要重新手动修改value值。如果一个test case中有多个地方需要这些值(比如,登陆之后的校验等),在修改的时候容易出现疏漏。此时需要将这些value值变量化,对应的命令是store,修改后的代码如下:

  • 分支:按照最开始的流程图,打开主页网址的时候可能出现两种情况,主页或登录页。所以需要先进行校验,只有在显示登录页时才需要输入用户名密码。selenium IDE中默认没有if这个命令,需要用gotoIf和label来代替,同时需要将要校验的条件变量化。

  • while循环:流程图的最后一部分是检验用户名密码是否正确,能否顺利登陆。再添加一对gotoIf和label可实现类似while的功能。我在两个gotoIf里采用了不同形式的条件语句,可以看到它允许ture、false判断,也允许直接与字符串比对等。

到这里,已经用selenium IDE实现了流程图的所有功能。总结一下:

  • 优点:
    • Easy to program:command形式固定,熟悉常用的几个command就可以完成一个基本的自动化脚本,没有复杂的语法。另外还有一些plugin可以应用,进一步完善功能。
    • Easy to locate:定位web element是web UI自动化中很重要的一环,selenium IDE可以直接在浏览器页面选择element获取定位方法,很是方便。
    • Easy to replay:编写好的test case可以直接运行,随时暂停、修改。大为降低了调试脚本的成本。
  • 缺点:
    • Firefox only:从各种官方文档来看,selenium IDE是针对Firefox推出的,虽然据说Chrome、IE浏览器也存在类似功能的插件,但毕竟不是官方主推,功能还待验证。
    • Not a recording tool:我一开始误以为这是一款录制脚本的工具,但使用后才明白IDE这个名字不是白叫的。录制的脚本距离可用于生产的自动化脚本还相差甚远,需要补充逻辑、变量化、添加校验等等。
    • Poor maintainability and repeating code:就以这段登录页面来说,虽然我们可以把这段脚本保存为一个test case,然后在所有需要它的test suite里包含这个test case,但这也意味着每次都要手动修改其中的常量信息,要比程序中方法调用复杂的多。

鉴于这些我问题,我决定把selenium IDE当做一个工具,而真正的自动化程序还是得靠selenium webdriver。

使用selenium webdriver的实现

与selenium IDE部分相对应,首先给出实现主流程的功能代码。其中selenium IDE命令与selenium webdriver方法对应:open -- get(), type -- sendKeys(), clickAndWait -- click() + wait.until()。selenium webdriver里定位web element需要借助findElement()方法和By类定义的locator。

public class TestSelenium {
    private WebDriver webDriver;
    String homePageURL = "https://...";

    @BeforeTest
    public void setup(){
        System.setProperty("webdriver.gecko.driver","/Users/.../geckodriver");
        webDriver = new FirefoxDriver();
    }

    @Test
    public void test(){
        webDriver.get(homePageURL);
        webDriver.findElement(By.id("userid")).sendKeys("Z...");
        webDriver.findElement(By.id("password")).sendKeys("W...");
        webDriver.findElement(By.id("btnActive")).click();
        WebDriverWait wait = new WebDriverWait(webDriver,60);
        wait.until(ExpectedConditions.titleIs("O..."));
    }

    @AfterTest
    public void quitDriver(){...}
}
  • 等待页面加载:selenium webdriver里的等待有两种,显示等待和隐式等待。代码中的wait.until就是显示等待的使用。根据源码注释,get方法会等待页面加载完全才会真正结束,这就说明get方法之后不需要显示等待来配合。

  • 参数变量化:变量化用户名、密码,同时将登录部分代码extract成新的private方法,便于日后复用。

@Test
public void test(){
    webDriver.get(homePageURL);
    signIn("Z...", "W...");
    WebDriverWait wait = new WebDriverWait(webDriver,5);
    wait.until(ExpectedConditions.titleIs("O..."));
    //等待5s,检查网页title,确认是否已登录成功跳转
}

private void signIn(String userid, String passwd) {
    webDriver.findElement(By.id("userid")).sendKeys(userid);
    webDriver.findElement(By.id("password")).sendKeys(passwd);
    webDriver.findElement(By.id("btnActive")).click();
}

  • 分支 & while循环:if 和 while语句均符合我们的习惯,无二次学习成本。
@Test
public void test(){
    webDriver.get(homePageURL);

    if(!webDriver.getTitle().equalsIgnoreCase("O...")) {
    //是否需要登录
        WebDriverWait wait = new WebDriverWait(webDriver, 5);
        LocalTime localTime = LocalTime.now();
        LocalTime deadLine = localTime.plusMinutes(1);
        while(LocalTime.now().isBefore(deadLine)) {
        //限制允许登录失败重试的时间,大多数情况下应该没有必要用这个while,是我想太多
            try {
                signIn("Z...", "W..."); //如果有多个同样性质的用户作为备选登录者,这里可改进为读入数据,一个失败后,可在下个循环更换另一个用户,重试。
                wait.until(ExpectedConditions.titleIs("O..."));
                //等待5s,检查网页title,确认是否已登录成功跳转
                System.out.println(webDriver.getTitle());
                return;//如果登录成功,则进行其他操作后退出
            } catch (TimeoutException e) {
                continue;//如果登录失败,则重新登录
            }
        }
        throw new TimeoutException();//超时,仍未登录成功,抛出异常
    }
    System.out.println(webDriver.getTitle());    
}

进行到这里,其实也已经实现了流程图的全部步骤。但仍让人不满意的是selenium webdriver对web element的定位冗余、不利于复用,以及test包含太多逻辑。为了进一步提高代码质量,Page Object Pattern 闪亮登场。

Page Object Pattern

这个模式指的是,将每个页面看做一个对象,这些对象有自己的属性(web element等)和方法( 信息提交、页面跳转等 action ),将这些信息放在一起,而test对应的方法负责想要测试的一个功能点,其内部生成对象实例、调用对象的方法、并负责校验功能是否符合预期。而PageFactory类则是实现Page Object Pattern必不可少的利器。

以我的这个登录流程为例,测试目标是输入主页网址可能产生的几种操作。这里涉及到两个页面:登录页(LoginPage)和主页(Homepage)。

  • LoginPage:登录页我们关心的元素为用户名输入框、密码输入框、登录按钮,动作为点击登录按钮提交信息。下面例子中给出的代码对应包含三个属性、一个动作的定义。其中三个属性的初始化并没有用之前提到的“webDriver.findElement(By.id("userid"))”,而是用注解@FindBy,这正是PageFactory的作用。
public class LoginPage {
    private WebDriver  webDriver;

    @FindBy(how = How.ID, using = "userid")
    private WebElement userid;

    @FindBy(how = How.ID, using = "password")
    private WebElement passwd;

    @FindBy(how = How.ID, using = "btnActive")
    private WebElement submitbtn;

    public LoginPage(WebDriver webDriver){
        this.webDriver = webDriver;
        PageFactory.initElements(webDriver,this);
        //初始化page object,传递webdriver,查找element
        //去掉这一句会产生“NullPointerException”
        //可以将这条语句移到test方法new page object部分,同时省略类定义中的webdriver属性,具体见下文代码        
    };

    public void submitLoginInfo(String username, String password) {
        userid.sendKeys(username);
        passwd.sendKeys(password);
        submitbtn.submit();
    }
  • HomePage:主页我们关心的元素主要是页面title或是任何可以可以用来判断当前页面是不是主页的特征元素,动作则是输入网址打开主页、验证主页特征元素。在selenium IDE版本里,这一点是通过验证当前页面的title来实现的。在下面的例子里,我用的是主页显示用户名的元素来验证成功登陆进入主页。
public class HomePage {
    WebDriver webDriver;
    private String username = "ZHRA-HRSPC1";
    private String password = "Welcome1";

    @FindBy(how = How.XPATH, using = "/html/body/div[2]/form/...")
    WebElement loggedUser;

    public HomePage(WebDriver webDriver, String... args) { 
        .... //构造函数
        if(args.length>1){
            username = args[0];
            password = args[1];
        }else if(args.length>0){
            username = args[0];
        }//传递用户名、密码,也可以写成构造函数重载
    }
    public void getHomePage(){    
        ....        
        webDriver.get(homePageUrl);        
        ....//同上文,设置WebDriverWait

                try {
                    loginPage.submitLoginInfo(username, password);
                    wait.until(ExpectedConditions.visibilityOf(loggedUser));//判断条件为loggedUser元素是否显示
                    return;
                } 
        ....

  • @Test:如下所示,真正的测试部分的代码就非常简单直观了。将登录页面跳转等自动逻辑隐藏起来,简化复用成本。
private WebDriver webDriver;
private HomePage homePage;

@Test
public void loginTest(){
    homePage.getHomePage();//new homePage的时候可以将自定义的用户名、密码作为参数传递
}

LoadableComponent

在page object pattern里,每个页面都可能涉及判断页面元素是否加载完成的问题。对于动态加载页面来说,这个工作量很繁琐,也不美观。selenium 提供了 LoadableComponent 基础类,作为page object pattern的扩展,提供更一致且优雅的解决方法。

具体方法为

  1. 声明类时继承LoadableComponent<>类
  2. 覆写load方法和isLoaded方法
  3. 调用类对象的get方法访问页面

load方法内包含的是访问页面的操作,isLoaded方法内包含的是检查页面元素是否加载成功的操作。从源代码可以看出,get方法首先调用isLoaded方法判断是否加载成功,没有成功的话会捕获Error,然后调用load方法加载,之后会再次调用isLoaded方法判断,并返回。如果第二次加载仍未成功则会抛出Error。

采用这种方法改写的page类及test方法。

  • LoginPage:
public class LoginPage extends LoadableComponent<LoginPage>{

    ....

    @Override
    protected void load() {}

    @Override
    protected void isLoaded() throws Error{
        try{
            new WebDriverWait(webDriver, 30).until(ExpectedConditions.visibilityOf(userid));
        }catch (WebDriverException ex){
            throw new Error("LoginPage was not successfully loaded in 30s");
        }
    }
}
  • HomePage:
public class HomePage extends LoadableComponent<HomePage> {

   ....

    @Override
    protected void load() {
        String homePageUrl = "https://...";
        webDriver.get(homePageUrl);
    }

    @Override
    protected void isLoaded() throws Error {
        if (!webDriver.getTitle().equalsIgnoreCase("O...") && !webDriver.getTitle().equalsIgnoreCase("Sign in")){
            throw new Error("neither homepage nor login page appears");
        }

        if (webDriver.getTitle().equalsIgnoreCase("Sign in")) {
            WebDriverWait wait = new WebDriverWait(webDriver, 5);
            try {
                 loginPage.get();
                 loginPage.submitLoginInfo(username,password);
                 wait.until(ExpectedConditions.visibilityOf(loggedUser));
            } catch (TimeoutException e) {
                 throw new TimeoutException("fail to login");
            }
        }

        if (!webDriver.getTitle().equalsIgnoreCase("O...")){
            throw new TimeoutException("fail to load homepage in almost 1 minute");
        }
    }
}
@Test
    public void loginTest(){
        homePage.get();
    }

未完成的改进

至此,针对一个我工作中常见的步骤--登录页面跳转,使用selenium IDE和webdriver进行了实现,同时尝试应用了page object pattern。

但我对现在的代码仍不是很满意,只是目前为止还没有找到进一步改进的方法。譬如,selenium get(url)方法可以打开一个页面并等待其加载完成,那么通过点击按钮跳转到全新页面时,有没有类似的方法可以等待新的页面加载完成呢?

如果日后我找到更简单的实现,会继续更新的,也希望有想法的同事能为我指点迷津。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 11 条回复
2417

点赞,书写条理很清晰,期待后续。

6c4add
16280fudax 回复

有道理~多谢~

110 Lihuazhang 将本帖设为了精华贴 10月12日 08:09
167d23

waitForElement(elementLocator) 等待元素出现 500ms扫描一次 把登陆后的一个标志元素写进去就好了

6c4add
167d23Innocence 回复

您说的waitForElement(elementLocator)是指把我的代码中wait.until(ExpectedConditions.visibilityOf(webElement))那部分代码封装成一个单独的方法吗?如果我理解的没错的话,这个当然可以。
只是我的问题是,是否有一个方法可以做到:点击一个button->页面跳转到全新页面,这个新页面加载过程中程序是block住的,就像get(url)打开全新页面那样,只有等到页面加载完成才会进入下一步。这样的话下一步检查标志元素没有出现,我们就可以判断新页面打开的不对,而不用担心等待时间不够元素还没有完全加载。

16280
6c4addcheryl.cai 回复

看样子你还是没好好琢磨透webdriver怎么工作的啊,给你个样例你看下:
每一步操作,工具都会自动等到你设置的超时时间(pageLoadTimeout、implicitlyWait)过了才会继续判定为失败,在此时间内会自动轮询检查的
对ajax局部刷新,一样有效

@Override
public void startWebDriver() {
    cleanBrowserProcess();
    try {
        initWebDriver();
        driver.manage().timeouts().pageLoadTimeout(maxLoadTime, TimeUnit.SECONDS);
        driver.manage().timeouts().setScriptTimeout(maxWaitfor, TimeUnit.SECONDS);
        driver.manage().timeouts().implicitlyWait(maxWaitfor, TimeUnit.SECONDS);
        driver.manage().window().maximize();
    } catch (Exception e) {
        LoggerUtils.error(e);
        throw new RuntimeException(e);
    }
}
6c4add
16280fudax 回复

pageLoadTimeout这个设置之前我确实没有注意到,就我刚刚查找的相关的selenium issues来看,这个设置在selenium 3 和FF55以下的版本存在bug。更新到最新的FF和selenium就可以设置了。

我试了在click命令之前更改这个参数,确实会产生“org.openqa.selenium.TimeoutException: Timed out waiting for page load.”。

但是,click这个命令,官方文档里写的是“If this causes a new page to load, you should discard all references to this element and any further operations performed on this element will throw a StaleElementReferenceException. Note that if click() is done by sending a native event (which is the default on most browsers/platforms) then the method will not wait for the next page to load and the caller should verify that themselves.”,这难道不是说它不会受设置的超时时间的限制吗?然后我就糊涂了。。。

我还是个新手,肯定还有很多理解不到的地方,请多多指教~

16280
6c4addcheryl.cai 回复

参考一下:http://www.it1352.com/313066.html
实际操作起来,我发现其实这种操作在【效果上】还是阻塞的,并非异步
为什么呢?换个角度思考,有了这个timeout,你在下一个page加载完成之前的任何操作(包括断言),也会有同样的超时设置作用于对你的element的加载等待,除非你故意在下一步操作之前把超时时间设置为1s或者更短~
所以我在脚本中基本不动这个设置,只有在初始化的时候通过全局设置一下,结合自己系统、环境的性能表现即可。

79a566

不知道为什么要想的如此复杂,selenium提供的方法就可以解决啊。

WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(webelement(ID,xpath,name,css等等));

这个方法用于,动态等待对象出现,如果在设置的时间段内没有出现,即返回false。
所以,你用get(url)之后,直接加上这个方法,判断跳转的登录页面是否出现了某元素。
贴一段我的登录代码:

/**
 * 登录
 * 
 */
public void login(String url, String email, String pwd) {
    // 打开登录网址
    webManager.NavigateTo(url);
    webManager.threadWait(3);
    // 浏览器最大化
    webManager.maximizeBroswer();
    webManager.threadWait(1);
    // 等待登录页面用户名输入框出现,60秒未出现则按超时处理
    webManager.dynamicWaitAppear(WebElementType.ID, "email", 60);
    // 输入用户名
    webManager.sendKeys(WebElementType.ID, "email", email);
    webManager.threadWait(1);
    // 输入密码
    webManager.sendKeys(WebElementType.ID, "pwd", pwd);
    webManager.threadWait(1);
    // 输入验证码
    webManager.sendKeys(WebElementType.ID, "jcaptcha", "1");
    webManager.threadWait(1);
    // 单击登录按钮
    webManager.click(WebElementType.ID, "login");
    webManager.threadWait(2);
}
79a566

这是等待对象出现的方法:

/**
     * 等待对象出现
     * 
     * @param type
     *            对象类型:id,name,css,xpath,class,linktext,tagname,partialLinkText
     * @param value
     *            对象的值
     * @param iTimeout
     *            超时时间
     * @return FALSE:对象未在规定时间内出现 TRUE:对象在规定时间内出现
     */
    public boolean dynamicWaitAppear(WebElementType type, String value, Integer iTimeout) {
        try {
            switch (type) {
            case ID:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.id(value)));
                break;
            case NAME:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.name(value)));
                break;
            case CSS:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.cssSelector(value)));
                break;
            case XPATH:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.xpath(value)));
                break;
            case CLASS:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.className(value)));
                break;
            case LINKTEXT:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.linkText(value)));
                break;
            case TAGNAME:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.tagName(value)));
                break;
            case PARTIALLINKTEXT:
                new WebDriverWait(webDriver, Integer.valueOf(iTimeout))
                        .until(ExpectedConditions.presenceOfElementLocated(By.partialLinkText(value)));
                break;
            default:
                break;
            }

            String message = "等待对象" + value + "出现成功.";
            LogHelper.LogMessage(Variable.DONE, "WebManager.DynamicWaitAppear", message);
        } catch (Exception ex) {
            String screenshot = LogHelper.Capscreenshot();
            String message = "等待对象" + value + "出现失败.错误详情请见:" + screenshot;
            LogHelper.LogMessage(Variable.FAILD, "WebManager.DynamicWaitAppear", message);
            softAssert.assertFalse(true);
        }
        return true;
    }
16280

单步等待的API封装,一般不赞成这么用

/**
   * Description: set element locate timeout.</BR> 
   * 内容描述:设置对象查找超时时间.
   *
   * @param seconds
   *            timeout in timeunit of seconds.
   */
  protected void setElementLocateTimeout(int seconds) {
      driver.manage().timeouts().implicitlyWait(seconds, TimeUnit.SECONDS);
  }

  /**
   * wait for the element visiable in timeout setting</BR> 
   * 在指定时间内等待,直到对象可见。
   *
   * @param by
   *            the element locator By
   * @param seconds
   *            timeout in seconds
   */
  protected boolean waitForElementVisible(By by, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.visibilityOfElementLocated(by)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for the element visiable in timeout setting</BR> 
   * 在指定时间内等待,直到对象可见。
   *
   * @param element
   *            the element to be found.
   * @param seconds
   *            timeout in seconds.
   */
  protected boolean waitForElementVisible(WebElement element, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.visibilityOf(element)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for the element not visiable in timeout setting</BR>
   * 在指定时间内等待,直到对象不可见。
   *
   * @param by
   *            the element locator.
   * @param seconds
   *            timeout in seconds.
   */
  protected boolean waitForElementNotVisible(By by, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.invisibilityOfElementLocated(by)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for the element present in timeout setting</BR> 
   * 在指定时间内等待,直到对象出现在页面上。
   *
   * @param by
   *            the element locator.
   * @param seconds
   *            timeout in seconds.
   */
  protected boolean waitForElementPresent(By by, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.presenceOfElementLocated(by)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for the element clickable in timeout setting</BR>
   * 在指定时间内等待,直到对象能够被点击。
   *
   * @param by
   *            the element locator By
   * @param seconds
   *            timeout in seconds
   */
  protected boolean waitForElementClickable(By by, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.elementToBeClickable(by)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for text appears on element in timeout setting</BR>
   * 在指定时间内等待,直到指定对象上出现指定文本。
   *
   * @param by
   *            the element locator By
   * @param text
   *            the text to be found of element
   * @param seconds
   *            timeout in seconds
   */
  protected boolean waitForTextOnElement(By by, String text, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.textToBePresentInElementLocated(by, text)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }

  /**
   * wait for text appears in element attributes in timeout setting</BR>
   * 在指定时间内等待,直到指定对象的某个属性值等于指定文本。
   *
   * @param by
   *            the element locator By
   * @param text
   *            the text to be found in element attributes
   * @param seconds
   *            timeout in seconds
   */
  protected boolean waitForTextOfElementAttr(By by, String text, int seconds) {
      try {
          setElementLocateTimeout(seconds);
          WebDriverWait wait = new WebDriverWait(driver, seconds, stepTimeUnit);
          return wait.until(ExpectedConditions.textToBePresentInElementValue(by, text)) != null;
      } finally {
          setElementLocateTimeout(maxWaitfor);
      }
  }
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册