<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>edwardzhou (Edward Zhou)</title>
    <link>https://ruby-china.org/edwardzhou</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>说说风险与危机</title>
      <description>&lt;p&gt;这么多年工作中，经历过许多经验教训，我喜欢对过去的事件进行回顾复盘，尝试从中找到问题所在，和有哪些解决之道。&lt;/p&gt;

&lt;p&gt;一直以来都想说说关于风险，是因为风险往往在你不认为有或意向不到的时候，它偏偏出现了，而细究起来，它的出现都是有规律，有蛛丝马迹可寻的，甚至是有“信号”提醒的。当风险出现后，一不留神，很快就会成为问题暴露出来，随着引发危机，影响有大有小，通常会往没有最糟只有更糟的局面走。&lt;/p&gt;

&lt;p&gt;我会列出我自身经历过的几种常见的风险。&lt;/p&gt;
&lt;h2 id="1. 团队扩张蕴藏的风险"&gt;1. 团队扩张蕴藏的风险&lt;/h2&gt;
&lt;p&gt;这个风险是最常见的，也是最容易忽略的。跟贴切的来说，被乐观所掩盖。&lt;/p&gt;

&lt;p&gt;为什么需要扩张团队？公司业务发展了、想上市融资了、打算上新项目了、部门间跑马圈地了.....，总之，就是形势一片大好，必须得扩大团队。&lt;/p&gt;

&lt;p&gt;Hold on please. 当你需要扩大团队与扩大规模时，&lt;strong&gt;风险&lt;/strong&gt; 就在这里。
看看实际例子：&lt;/p&gt;

&lt;p&gt;07 年我加入了 W 公司，做财富管理软件的，客户是香港的一些银行，公司有两个主要客户和主要项目是成功案例，其中一个客户项目是我负责的，由此签下了其他银行的几个单子，公司从年中的四十几个人，到年底扩充到八十几个，现有办公室的座位已经不足，需要再租用办公室，我的项目并没有增加人手，所有加的人手都投入到新项目中，BOSS 也是红光满面的押宝新项目。
但是，扩大一倍的人数，开支也就增加了一倍不止。然而，并没有带来成倍的产出，反而更低。迟迟无法交付项目，应收款也就无法回收，最终导致现金流出现问题，开始拖欠工资，导致雪崩效应。公司付出惨重代价，到我离开之时，公司的最终有效客户仅剩我跟进的那个，人数也收缩为三十人不到，业务严重受创，几年都没缓过劲来，后来听说卖给另一家外资公司。&lt;/p&gt;

&lt;p&gt;08 年加入港资 J 公司，公司业务是海外的 CP 业务，在甚至成立 R&amp;amp;D 中心，目的是上市时需要有一定的研发团队数量，公司开始的一年多时间，人数稳定在二十几三十不到的人数，分两个团队，我们经过一年左右的时间，令到深圳团队的工作效率、工作质量、工作方式与香港团队保持一致，甚至比他们还好，同样的工作更低的成本。之后香港团队看到成果，认为这种模式很好 copy，一时间各个业务团队都跑过来跑马圈地，人数迅速扩张到五十多个，办公空间完全不够，随即雄心勃勃的搬到竹子林租下可容纳一百多个人的一层楼，以便容下更多的团队，人数继续扩张到最多将近 100 人，简直巅峰时刻，这个规模的开销一年就得七八百万以上，三年下来共烧掉两千万左右。把团队管理想得太 easy 了，几个团队 build 不起来，挂了，最后仅保留 4 个团队五十多个人，办公室闲置一半。最后，冲击上市失败，R&amp;amp;D 收缩回香港。&lt;/p&gt;

&lt;p&gt;后来的 Z 公司，具体不表了，都是同样的模式，也是从四十几号人，年中开始一直加人，到年底增加到八十几个，公司是一片繁荣景象，各个项目风声水起的，一个月的开支增至一百多两百万，而春节刚结束，大大小小规划了十来个立项；当人数在增加时，我心里已经在打鼓，仿佛看到某种历史在重演，事实如此，新业务还没搞起来，钱已烧光。反正我的职位注定是其中一个背锅侠。&lt;/p&gt;

&lt;p&gt;历史何其相似。根究其原因，当公司的企业文化、流程、规模不够强大成熟时，对新人的消化能力是有限的。通常 10% 的比例是一个临界点。超过这个比例，就会消化不良，流程变得不顺畅，开始官僚，效率质量反而严重下降，更别说吃下同体积的新人，直接噎死。&lt;/p&gt;

&lt;p&gt;有趣的现象，越加人，事情越干不完，所有人会表现的更加忙，因而需要更多人，循环.... ，实际上真正在产出的就只其中几个。其他人如果不表现忙，岂不意味着 Ta 是多余的？为了保住工作，没事也得整点事来忙忙。&lt;/p&gt;
&lt;h4 id="因此，当团队需要扩张的时候，请警惕，认真思考一下必要性与自身的消化能力。是否是我们的业务铺的太开，不是是可以暂停低价值业务，集中专注最核心最有发展潜力的业务？"&gt;因此，当团队需要扩张的时候，请警惕，认真思考一下必要性与自身的消化能力。是否是我们的业务铺的太开，不是是可以暂停低价值业务，集中专注最核心最有发展潜力的业务？&lt;/h4&gt;&lt;h4 id="当公司看起来一片繁荣景象时，也请冷静下来，审视自己，这个繁荣时真实繁荣，还是假象繁荣？ 尤其是急加人后出现的繁荣。"&gt;当公司看起来一片繁荣景象时，也请冷静下来，审视自己，这个繁荣时真实繁荣，还是假象繁荣？尤其是急加人后出现的繁荣。&lt;/h4&gt;
&lt;hr&gt;
&lt;h2 id="2. 项目进展异常顺利的风险"&gt;2. 项目进展异常顺利的风险&lt;/h2&gt;
&lt;p&gt;当项目进展非常顺利的时候，往往里面隐藏着风险。我们在做敏捷迭代，两周 10 个工作日一个迭代，期间，有一个迭代，在第四五个工作日开始进展非常顺利，持续集成一切正常，自动测试用例全绿，而且还可以领新任务。好到让我觉得有点不真实，果然，到了第七个工作日快结束是，团队的一个成员，发现前端小伙伴为了自己开发方便，把相当一部分的测试用例全给注释掉了。第八个工作日早上，站立会议交流这个事情，回去重新打开测试用例，一片红啊。结果到迭代评审会时仍有好几个已知 BUG，真是难为 PO 收下。&lt;/p&gt;

&lt;p&gt;以前的工作中，都会碰到类似的情况。&lt;/p&gt;
&lt;h4 id="当项目突然很顺，进度突然很好时，请踩踩刹车，我敢肯定，很大部分是有问题在里面的。"&gt;当项目突然很顺，进度突然很好时，请踩踩刹车，我敢肯定，很大部分是有问题在里面的。&lt;/h4&gt;
&lt;hr&gt;
&lt;h2 id="3. 人员风险"&gt;3. 人员风险&lt;/h2&gt;
&lt;p&gt;团队成员通常会在你认为没有问题的时候出问题，突然就会告诉你 Ta 要离职，而这个人往往也是你不认为会出问题的。&lt;/p&gt;

&lt;p&gt;这个话题比较敏感，具体案例具体原因就不多说。但是，实际上总有一些迹象在暗示这个问题，请工作中用心去感受。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 替代风险"&gt;4. 替代风险&lt;/h2&gt;
&lt;p&gt;请记住这个世界离了谁，地球依旧在旋转，太阳依旧从东方升起。当你觉得自己无可代替时，你已给自己埋下了风险。事实上，在一个公司里任何人都是可以替代的，不同的是替代的代价大小而已，所以不要太高估自己的价值。一个企业，只有一个人是不可替代的，那个人就是公司的老板、创始人，因为他押上了自己的身家。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;风险无处不在，有问题才是正常的，太正常的才是有问题的。
还有许多其他风险无法一一列举，这里只是说说我自己遭遇的坑失败的案例，来减少他人的失败成本。同时，时刻提醒自己。我的印象最深刻的就是《奋斗》里陆涛撞车时，老徐对他说的那段话：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“我要你记住一件事，那就是记住刹车……还有就是，并不是你有把握的事就是很明确的，你觉得你会开了，但有时候形势会差强人意，你懂吗？你要学会避免失控。失控我一再强调，失控那是最坏的情况，因为谁也不知道失控以后会发生什么事，这是你最需要学的，也是人生最关键的，懂吗”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;告诫自己，在不确定或者太顺利时，踩刹车，让自己有一个审视的机会。&lt;/p&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Tue, 08 Aug 2017 14:39:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/33764</link>
      <guid>https://ruby-china.org/topics/33764</guid>
    </item>
    <item>
      <title>Elixir Phoenix 如何用 10 分钟 50 行代码快速撸一个 http 透传通知服务</title>
      <description>&lt;p&gt;我们购买 Github 服务来托管源代码，用 Jenkins 来做持续集成，因此需要让 github 能通知 Jenkins 各种通知事件，触发持续集成构建。&lt;/p&gt;

&lt;p&gt;公司网络环境为
Internet &amp;lt;------ 光猫 &amp;lt;------  路由器/交换机 &amp;lt;------ 内网
光猫是是电信提供的 100M 光纤，自动拨号上网，网段 192.168.1.x，不是路由器控制拨号上网；路由器与内网为 192.168.2.x。&lt;/p&gt;

&lt;p&gt;Jenkins 部署在内网上的 CentOS 主机，IP 为 192.168.2.231。&lt;/p&gt;

&lt;p&gt;由于光猫是被电信阉割了的，许多功能（如端口转发）都被屏蔽了，而路由器在上设置端口转发也起不到作用。&lt;/p&gt;

&lt;p&gt;即在 CentOS 上使用花生壳之类的，也无法让 Github 直接连通内网的 Jenkins。&lt;/p&gt;

&lt;p&gt;我们只能通过让 Jenkins 去定时轮询 Github 感知代码合并请求。这种方式有两个问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;及时性不够；&lt;/li&gt;
&lt;li&gt;代码合并事件无法感知；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为了弥补这个问题，我们的 CI 半手工触发，有些蛋疼。&lt;/p&gt;

&lt;p&gt;最近正好在看 Elixir 和 Phoenix, 两者分别与 Ruby 和 RubyOnRails 相对应。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-82e72c591c96b875.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="elixir_logo.png"&gt;&lt;/p&gt;

&lt;p&gt;传送门：
&lt;a href="http://elixir-lang.org/" rel="nofollow" target="_blank"&gt;http://elixir-lang.org/&lt;/a&gt;
&lt;a href="http://www.phoenixframework.org/" rel="nofollow" target="_blank"&gt;http://www.phoenixframework.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;有一句话说得很好，当你手里拿着一把锤子，但看到什么都是钉子。😂&lt;/p&gt;

&lt;p&gt;因此，打算自己快速撸一个透传通知服务给 github -&amp;gt; Jenkins 用。&lt;/p&gt;

&lt;p&gt;方案比较简单，用 Elixir Phoenix 框架打一个应用 jenkins_hook_proxy，提供 web 服务与 websocket 服务，运行在公网云主机上; 内网 forwarder 用 websocket 连接 jenkins_hook_proxy, 
1) Jenkins_hook_proxy 收到 GitHub 的通知时，将请求打包由 websocket 通知会内网，
2) 内网 forwarder 收到 websocket 通知后，拆解请求后转给 Jenkins
3) Jenkins 完成构建后，将构建结果通知 GitHub&lt;/p&gt;

&lt;p&gt;GitHub ---http post--&amp;gt; [jenkins_hook_proxy] &amp;lt;---websocket---&amp;gt; [forwarder] ----http post----&amp;gt; Jenkins ----(CI 构建结构) http post----&amp;gt; GitHub&lt;/p&gt;

&lt;p&gt;由于 Phoenix 的 channel 为提供了非常方便且健壮的 web 前端浏览器 js 脚本，为了能直接利用它的 js 脚本，forwarder 就用 nodejs 编写。&lt;/p&gt;

&lt;p&gt;整个过程下来，发现 jenkins_hook_proxy 和 forwarder 我们写的代码也就 20 句左右。&lt;/p&gt;
&lt;h2 id="环境准备"&gt;环境准备&lt;/h2&gt;&lt;h2 id="Erlang 安装"&gt;Erlang 安装&lt;/h2&gt;
&lt;p&gt;CentOS 上安装 Erlang 麻烦些，需要下载 pre-built binary package 安装
进入 &lt;a href="https://www.erlang-solutions.com/resources/download.html" rel="nofollow" target="_blank"&gt;https://www.erlang-solutions.com/resources/download.html&lt;/a&gt; 下载 19.3 的 rpm 包，然后用 root 用户安装 (sudo rpm -i eslxxx.rpm), 根据提示安装所需的依赖包 (eg. wxBase, wxGTK 等)。&lt;/p&gt;
&lt;h2 id="Elixir"&gt;Elixir&lt;/h2&gt;
&lt;p&gt;下载 elixir 源码 &lt;a href="https://github.com/elixir-lang/elixir/archive/v1.4.2.zip" rel="nofollow" target="_blank"&gt;https://github.com/elixir-lang/elixir/archive/v1.4.2.zip&lt;/a&gt;
解压后，make &amp;amp;&amp;amp; sudo make install 安装。
安装完运行 iex --version 看看是否正常。&lt;/p&gt;
&lt;h2 id="安装 Phoenix framework"&gt;安装 Phoenix framework&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://www.phoenixframework.org/docs/installation" rel="nofollow" target="_blank"&gt;http://www.phoenixframework.org/docs/installation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ mix local.hex&lt;/code&gt;
由于安装的是 1.3rc 版，包为 phx_new.ex。phoenix_new.ez 是 1.2.x 版本。
&lt;code&gt;$ mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="安装NodeJS"&gt;安装 NodeJS&lt;/h2&gt;
&lt;p&gt;phoenix 要求安装的有 NodeJS 用于编译 assets。
&lt;code&gt;$ sudo yum install node&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="创建运行在外网的项目 Jenkins_hook_proxy"&gt;创建运行在外网的项目 Jenkins_hook_proxy&lt;/h2&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix phx.new jenkins_hook_proxy --database mysql

* creating jenkins_hook_proxy/config/config.exs
* creating jenkins_hook_proxy/config/dev.exs
* creating jenkins_hook_proxy/config/prod.exs
* creating jenkins_hook_proxy/config/prod.secret.exs
* creating jenkins_hook_proxy/config/test.exs
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/application.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/channels/user_socket.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/views/error_helpers.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/views/error_view.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/endpoint.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/router.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/web.ex
* creating jenkins_hook_proxy/mix.exs
* creating jenkins_hook_proxy/README.md
* creating jenkins_hook_proxy/test/support/channel_case.ex
* creating jenkins_hook_proxy/test/support/conn_case.ex
* creating jenkins_hook_proxy/test/test_helper.exs
* creating jenkins_hook_proxy/test/web/views/error_view_test.exs
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/gettext.ex
* creating jenkins_hook_proxy/priv/gettext/en/LC_MESSAGES/errors.po
* creating jenkins_hook_proxy/priv/gettext/errors.pot
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/repo.ex
* creating jenkins_hook_proxy/priv/repo/seeds.exs
* creating jenkins_hook_proxy/test/support/data_case.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/controllers/page_controller.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/templates/layout/app.html.eex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/templates/page/index.html.eex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/views/layout_view.ex
* creating jenkins_hook_proxy/lib/jenkins_hook_proxy/web/views/page_view.ex
* creating jenkins_hook_proxy/test/web/controllers/page_controller_test.exs
* creating jenkins_hook_proxy/test/web/views/layout_view_test.exs
* creating jenkins_hook_proxy/test/web/views/page_view_test.exs
* creating jenkins_hook_proxy/.gitignore
* creating jenkins_hook_proxy/assets/brunch-config.js
* creating jenkins_hook_proxy/assets/css/app.css
* creating jenkins_hook_proxy/assets/css/phoenix.css
* creating jenkins_hook_proxy/assets/js/app.js
* creating jenkins_hook_proxy/assets/js/socket.js
* creating jenkins_hook_proxy/assets/package.json
* creating jenkins_hook_proxy/assets/static/robots.txt
* creating jenkins_hook_proxy/assets/static/images/phoenix.png
* creating jenkins_hook_proxy/assets/static/favicon.ico

Fetch and install dependencies? [Yn] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;敲回车安装依赖包。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* running mix deps.get
* running mix deps.compile
* running cd assets &amp;amp;&amp;amp; npm install &amp;amp;&amp;amp; node node_modules/brunch/bin/brunch build

We are all set! Run your Phoenix application:

    $ cd jenkins_hook_proxy
    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server

Before moving on, configure your database in config/dev.exs and run:

    $ mix ecto.create

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 Channel (websocket 服务) JenkinsChannel.
JenkinsChannel 仅作为 websocket 通道用于回传信息给内网，使用默认生成的代码就足够了。不是我们自己动手写的，不纳入行数计算 😜&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
$ cd Jenkins_hook_proxy
$ mix phx.gen.channel Jenkins
* creating lib/jenkins_hook_proxy/web/channels/jenkins_channel.ex
* creating test/web/channels/jenkins_channel_test.exs

Add the channel to your `lib/jenkins_hook_proxy/web/channels/user_socket.ex` handler, for example:

    channel "jenkins:lobby", JenkinsHookProxy.Web.JenkinsChannel


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑 user_socket.ex , 把上面那句话加进去。
这算一行代码吧 😄&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;vi&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jenkins_hook_proxy&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;user_socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ex&lt;/span&gt;

&lt;span class="n"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;JenkinsHookProxy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;UserSocket&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Socket&lt;/span&gt;

  &lt;span class="c1"&gt;## Channels&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt; &lt;span class="s2"&gt;"jenkins:lobby"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;JenkinsHookProxy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;JenkinsChannel&lt;/span&gt;

  &lt;span class="c1"&gt;## Transports&lt;/span&gt;
  &lt;span class="n"&gt;transport&lt;/span&gt; &lt;span class="ss"&gt;:websocket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Phoenix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Transports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;WebSocket&lt;/span&gt;
  &lt;span class="c1"&gt;# transport :longpoll, Phoenix.Transports.LongPoll&lt;/span&gt;

  &lt;span class="c1"&gt;# Socket params are passed from the client and can&lt;/span&gt;
  &lt;span class="c1"&gt;# be used to verify and authenticate a user. After&lt;/span&gt;
  &lt;span class="c1"&gt;# verification, you can put default assigns into&lt;/span&gt;
  &lt;span class="c1"&gt;# the socket that will be set for all channels, ie&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;#     {:ok, assign(socket, :user_id, verified_user_id)}&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# To deny connection, return `:error`.&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# See `Phoenix.Token` documentation for examples in&lt;/span&gt;
  &lt;span class="c1"&gt;# performing token verification on connect.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Socket id's are topics that allow you to identify all sockets for a given user:&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;#     def id(socket), do: "user_socket:#{socket.assigns.user_id}"&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# Would allow you to broadcast a "disconnect" event and terminate&lt;/span&gt;
  &lt;span class="c1"&gt;# all active sockets and channels for a given user:&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;#     JenkinsHookProxy.Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# Returning `nil` makes this socket anonymous.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_socket&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;do: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们打算支持内网有多个 jenkins 的情况，同时，为了一点点安全考虑，每个 jinkins 我们分配一个随机字符串 (如 uuid) 作为 token, 至少一定程度上避免一些无效调用。用数据库来保存这个配置信息。&lt;/p&gt;

&lt;p&gt;不需要 HTML，因此直接用 json 创建整个脚手架 (controller, model ...)&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix phx.gen.json Jenkins Callback callbacks host_id:string token:string callback_url:string

* creating lib/jenkins_hook_proxy/web/controllers/callback_controller.ex
* creating lib/jenkins_hook_proxy/web/views/callback_view.ex
* creating test/web/controllers/callback_controller_test.exs
* creating lib/jenkins_hook_proxy/web/views/changeset_view.ex
* creating lib/jenkins_hook_proxy/web/controllers/fallback_controller.ex
* creating lib/jenkins_hook_proxy/jenkins/callback.ex
* creating priv/repo/migrations/20170330015124_create_jenkins_callback.exs
* creating lib/jenkins_hook_proxy/jenkins/jenkins.ex
* creating test/jenkins_test.exs

Add the resource to your api scope in lib/jenkins_hook_proxy/web/router.ex:

    resources "/callbacks", CallbackController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate


&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建数据库并执行迁移脚本创建 callbacks 表&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix ecto.create
$ mix ecto.migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑 lib/jenkins_hook_proxy/jenkins/jenkins.ex
在 get_callback 方法后面，添加 get_callback_by_host_id&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi  lib/jenkins_hook_proxy/jenkins/jenkins.ex

  @doc """
  Gets a single callback by host_id.

  Raises `Ecto.NoResultsError` if the Callback does not exists.

  """
  def get_callback_by_host_id!(host_id), do: Repo.get_by!(Callback, host_id: host_id)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑 callback_controller, 把现有的方法 index create show update delete 统统删掉，然后重写 create 方法，如下：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi lib/jenkins_hook_proxy/web/controllers/callback_controller.ex

defmodule JenkinsHookProxy.Web.CallbackController do
  use JenkinsHookProxy.Web, :controller

  alias JenkinsHookProxy.Jenkins
  alias JenkinsHookProxy.Jenkins.Callback

  action_fallback JenkinsHookProxy.Web.FallbackController

  def create(conn, %{"host_id" =&amp;gt; host_id, "token" =&amp;gt; token} = params) do
    with %Callback{} = callback &amp;lt;- Jenkins.get_callback_by_host_id!(host_id) do
      case callback.token do
        ^token -&amp;gt;
          # token与数据库的token匹配
          payload = %{
            # 提取github请求头(x- 开头 和 user-agent), 用于内网转发
            github_headers: conn.req_headers
                            |&amp;gt; Enum.filter(fn({k, v}) -&amp;gt; to_string(k) |&amp;gt; String.starts_with?(["x-", "user-agent"]) end)
                            |&amp;gt; Enum.into(%{}),
            jenkins: %{
              # 内网Jenkins Hook地址
              "callback_url": callback.callback_url
            },
            # 原生请求头, 无实际用途，方便调试查看用
            raw_headers: conn.req_headers |&amp;gt; Enum.into(%{}),
            # github请求参数，剔除 host_id 与 token
            body: params |&amp;gt; Map.drop(["token", "host_id"])
          }
          # websocket广播，向通道 jenkins:lobby 回传 jenkins_msg 事件
          JenkinsHookProxy.Web.Endpoint.broadcast! "jenkins:lobby", "jenkins_msg", payload

          # 返回正常给github
          conn
          |&amp;gt; json(%{result: "ok"})

       _ -&amp;gt;
        # token无效，返回403给github
        conn
        |&amp;gt; put_status(:forbidden)
        |&amp;gt; json(%{error: "token not matched."})
      end
    end
  end
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些代码中，真正发挥作用的是&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# websocket广播，向通道 jenkins:lobby 回传 jenkins_msg 事件
JenkinsHookProxy.Web.Endpoint.broadcast! "jenkins:lobby", "jenkins_msg", payload 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Jenkins.get_callback_by_host_id!(host_id) 当数据没有相应 host_id 记录时，会抛出异常 Ecto.NoResultsError。&lt;/p&gt;

&lt;p&gt;action_fallback 机制是 Phoenix 1.3 的一个非常不错的特性，controller 中我们可以只写正常业务逻辑代码，错误处理移交给 fallback_controller 去集中处理，这一点对 API 类应用尤其实用。&lt;/p&gt;

&lt;p&gt;修改 fallback_controller.ex，加入 对 Ecto.NoResultsError 返回 404&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi lib/jenkins_hook_proxy/web/controllers/fallback_controller.ex


  def call(conn, {:error, %Ecto.NoResultsError{} = _error}) do
    conn
    |&amp;gt; put_status(:not_found)
    |&amp;gt; json(%{error: "Jenkins not found"})
  end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编辑路由，把 router.ex 中最后的 scope "api" 段打开并加入 callback&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi lib/jenkins_hook_proxy/web/router.ex

  # Other scopes may use custom stacks.
  scope "/api", JenkinsHookProxy.Web do
    pipe_through :api
    post "/callbacks/:host_id/:token", CallbackController, :create
  end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以运行 mix phx.routes 检查一下路由&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mix phx.routes


    page_path  GET   /                               JenkinsHookProxy.Web.PageController :index
callback_path  POST  /api/callbacks/:host_id/:token  JenkinsHookProxy.Web.CallbackController :create

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jenkins_hook_proxy 的功能已经全部完成。&lt;/p&gt;

&lt;p&gt;我们手动编辑添加的代码，去掉空行、注释行后，代码也就 50 行左右，如果按代码语句 (为了阅读看起来舒服，一条代码语句会分成多行来写)，都不足 20 条。&lt;/p&gt;

&lt;p&gt;像这种，就是一条代码语句，却占了 4 行&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# token无效，返回403给github
conn
|&amp;gt; put_status(:forbidden)
|&amp;gt; json(%{error: "token not matched."})
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="运行Jenkins_hook_proxy测试"&gt;运行 Jenkins_hook_proxy 测试&lt;/h3&gt;
&lt;p&gt;用命令行交互方式启动，添加一条配置数据
host_id: test
token: abc123
callback_url: &lt;a href="http://192.168.2.231:8020/github-webhook" rel="nofollow" target="_blank"&gt;http://192.168.2.231:8020/github-webhook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;JenkinsHookProxy.Jenkins.create_callback(%{host_id: "test", token: "abc123", callback_url: "&lt;a href="http://192.168.2.231:8020/github-webhook" rel="nofollow" target="_blank"&gt;http://192.168.2.231:8020/github-webhook&lt;/a&gt;"})&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ iex -S mix phx.server
Erlang/OTP 19 [erts-8.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

[info] Running JenkinsHookProxy.Web.Endpoint with Cowboy using http://0.0.0.0:4000
Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)&amp;gt; 16:37:04 - info: compiled 6 files into 2 files, copied 3 in 811 ms

nil
iex(2)&amp;gt; JenkinsHookProxy.Jenkins.create_callback(%{host_id: "test", token: "abc123", callback_url: "http://192.168.2.231:8020/github-webhook"})
[debug] QUERY OK db=5.2ms
INSERT INTO `jenkins_callbacks` (`callback_url`,`host_id`,`token`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["http://192.168.2.231:8020/github-webhook", "test", "abc123", {{2017, 3, 30}, {8, 46, 9, 810223}}, {{2017, 3, 30}, {8, 46, 9, 816571}}]
{:ok,
 %JenkinsHookProxy.Jenkins.Callback{__meta__: #Ecto.Schema.Metadata&amp;lt;:loaded, "jenkins_callbacks"&amp;gt;,
  callback_url: "http://192.168.2.231:8020/github-webhook", host_id: "test",
  id: 3, inserted_at: ~N[2017-03-30 08:46:09.810223], token: "abc123",
  updated_at: ~N[2017-03-30 08:46:09.816571]}}
iex(3)&amp;gt; 

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="创建运行在内网的项目 Forwarder (nodejs)"&gt;创建运行在内网的项目 Forwarder (nodejs)&lt;/h2&gt;
&lt;p&gt;启动另外一个命令行窗口。&lt;/p&gt;
&lt;h3 id="创建项目目录"&gt;创建项目目录&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ mkdir forwarder
$ cd forwarder
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="创建package.json"&gt;创建 package.json&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi package

{
  "name": "forwarder",
  "version": "0.0.1",
  "private": false
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="添加依赖项"&gt;添加依赖项&lt;/h3&gt;
&lt;p&gt;phoneix 的 js 是用 ES6 风格写的，因此需要引入 babel 转换。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm install --save babel-cli 
$ npm install --save babel-preset-env 
$ npm install --save babel-preset-es2015
$ npm install --save babel-preset-es2016
$ npm install --save ws
$ npm install --save node-fetch
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="jenkins_hook_proxy中复制需要的js"&gt;jenkins_hook_proxy 中复制需要的 js&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cp &amp;lt;jenkins_hook_proxy&amp;gt;/assets/js/socket.js .
$ cp &amp;lt; jenkins_hook_proxy&amp;gt;/deps/phoenix/assets/js/phoenix.js .
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="修改 phoenix.js"&gt;修改 phoenix.js&lt;/h3&gt;
&lt;p&gt;phoenix.js 本身是运行在浏览器中的，nodejs 并没有 window 对象，也没有 window.WebSocket。 
因此我们需要修改 phoenix.js 注入 window 及 window.WebSocket。&lt;/p&gt;

&lt;p&gt;把下面这句话插入到 192 行 class Push {  之前。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let window = {WebSocket: require('ws')};

class Push {
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="编辑 socket.js"&gt;编辑 socket.js&lt;/h3&gt;
&lt;p&gt;在 socket.js 中实现 channel 订阅与对服务器传回的 Jenkins_msg 事件进行处理。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi socket.js

// NOTE: The contents of this file will only be executed if
// you uncomment its entry in "web/static/js/app.js".

// To use Phoenix channels, the first step is to import Socket
// and connect at the socket path in "lib/my_app/endpoint.ex":
import {Socket} from "./phoenix";

import fetch from "node-fetch";

let socket = new Socket("ws://localhost:4000/socket", {params: {token: ''}})

// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "web/router.ex":
//
//     pipeline :browser do
//       ...
//       plug MyAuth
//       plug :put_user_token
//     end
//
//     defp put_user_token(conn, _) do
//       if current_user = conn.assigns[:current_user] do
//         token = Phoenix.Token.sign(conn, "user socket", current_user.id)
//         assign(conn, :user_token, token)
//       else
//         conn
//       end
//     end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "web/templates/layout/app.html.eex":
//
//     &amp;lt;script&amp;gt;window.userToken = "&amp;lt;%= assigns[:user_token] %&amp;gt;";&amp;lt;/script&amp;gt;
//
// You will need to verify the user token in the "connect/2" function
// in "web/channels/user_socket.ex":
//
//     def connect(%{"token" =&amp;gt; token}, socket) do
//       # max_age: 1209600 is equivalent to two weeks in seconds
//       case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
//         {:ok, user_id} -&amp;gt;
//           {:ok, assign(socket, :user, user_id)}
//         {:error, reason} -&amp;gt;
//           :error
//       end
//     end
//
// Finally, pass the token on connect as below. Or remove it
// from connect if you don't care about authentication.

socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("jenkins:lobby", {})

channel.on("jenkins_msg", payload =&amp;gt; {
  channel.lastPayload = payload;
  let url = payload.jenkins.callback_url;
  let headers = Object.assign({}, {"content-type": "application/json"}, payload.github_headers);

  console.log(`[${Date()}]\n ${JSON.stringify(payload, null, "  ")}`);
  fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(payload.body)
  })
    .then( (e) =&amp;gt; console.log(e) );
});

channel.join()
  .receive("ok", resp =&amp;gt; { console.log("Joined successfully", resp) })
  .receive("error", resp =&amp;gt; { console.log("Unable to join", resp) })

export {socket, channel};

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="创建 app.js"&gt;创建 app.js&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi app.js

import socket from "./socket"
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="添加.babelrc"&gt;添加.babelrc&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ vi .babelrc

{
  "presets": ["env", "es2015"]
}

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="运行 forwarder 测试"&gt;运行 forwarder 测试&lt;/h3&gt;
&lt;p&gt;ES6 语法，需要用 babel-node.js app.js 来转译执行，并看到结果。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./node_modules/babel-cli/bin/babel-node.js app.js
Joined successfully {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到上面的 Joined successfully {} ，说明已经成功连接上 Jenkins_hook_proxy。&lt;/p&gt;

&lt;p&gt;在 jenkins_hook_proxy 的运行控制台中，也能看到相应的输出：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex(3)&amp;gt; [info] JOIN "jenkins:lobby" to JenkinsHookProxy.Web.JenkinsChannel
  Transport:  Phoenix.Transports.WebSocket
  Parameters: %{}
[info] Replied jenkins:lobby :ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再开一个命令行窗口，模拟 github 发起请求&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -H "Content-Type: application/json" -d '{"foo": "bar"}' http://localhost:4000/api/callbacks/test/abc123
{"result":"ok"}                                                                                                         
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;jenkins_hook_proxy 运行输出可以看到日志信息：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex(12)&amp;gt; [info] POST /api/callbacks/test/abc123
[debug] Processing with JenkinsHookProxy.Web.CallbackController.create/2
  Parameters: %{"foo" =&amp;gt; "bar", "host_id" =&amp;gt; "test", "token" =&amp;gt; "abc123"}
  Pipelines: [:api]
[debug] QUERY OK source="jenkins_callbacks" db=0.5ms
SELECT j0.`id`, j0.`callback_url`, j0.`host_id`, j0.`token`, j0.`inserted_at`, j0.`updated_at` FROM `jenkins_callbacks` AS j0 WHERE (j0.`host_id` = ?) ["test"]
[info] Sent 200 in 998µs

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;forwarder 的运行输出也能看到日志信息：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Thu Mar 30 2017 17:10:25 GMT+0800 (CST)]
 {
  "raw_headers": {
    "user-agent": "curl/7.51.0",
    "host": "localhost:4000",
    "content-type": "application/json",
    "content-length": "14",
    "accept": "*/*"
  },
  "jenkins": {
    "callback_url": "http://192.168.2.231:8020/github-webhook"
  },
  "github_headers": {
    "user-agent": "curl/7.51.0"
  },
  "body": {
    "foo": "bar"
  }
}
(node:47869) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): FetchError: request to http://192.168.2.231:8020/github-webhook failed, reason: connect ECONNREFUSED 192.168.2.231:8020

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后这个错误，是因为 &lt;a href="http://192.168.2.231:8020/github-webhook" rel="nofollow" target="_blank"&gt;http://192.168.2.231:8020/github-webhook&lt;/a&gt; jenkins 服务器没打开。
但已经能说明，整条链路是正常的了。&lt;/p&gt;
&lt;h3 id="测试无效token"&gt;测试无效 token&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -H "Content-Type: application/json" -d '{"foo": "bar"}' http://localhost:4000/api/callbacks/test/abc   
{"error":"token not matched."}                                                                                      
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id="总结"&gt;总结&lt;/h3&gt;
&lt;p&gt;真实环境部署
jenkins_hook_proxy 运行在外网云主机上;&lt;/p&gt;

&lt;p&gt;forwarder 运行在内网主机上，可以直接在 jenkins 的机器上跑。&lt;/p&gt;

&lt;p&gt;反正我们已经是使用这么使用了。&lt;/p&gt;

&lt;p&gt;代码可以上 github 获取
&lt;a href="https://github.com/edwardzhou/jenkins_hook_proxy" rel="nofollow" target="_blank"&gt;https://github.com/edwardzhou/jenkins_hook_proxy&lt;/a&gt;
&lt;a href="https://github.com/edwardzhou/jenkins-forwarder" rel="nofollow" target="_blank"&gt;https://github.com/edwardzhou/jenkins-forwarder&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;#注意
上面的代码是十几分钟撸出来的玩具，帮助我们偿还持续集成的技术债务而已。
对于其他问题和副作用概不承担责任 😜😜😜&lt;/p&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Mon, 07 Aug 2017 09:28:55 +0800</pubDate>
      <link>https://ruby-china.org/topics/33750</link>
      <guid>https://ruby-china.org/topics/33750</guid>
    </item>
    <item>
      <title>敏捷实践 (4) - 我们是如何改进 AC</title>
      <description>&lt;p&gt;在 &lt;a href="http://www.jianshu.com/p/fd17c1bb6806" rel="nofollow" target="_blank" title=""&gt;敏捷实践 (1)&lt;/a&gt; 中，出于介绍目的，测试用例实现的都相对简单。&lt;/p&gt;

&lt;p&gt;而实际上验收标准测试用例也并不复杂，百分之八九十的动作无非是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;跳转某个界面&lt;/li&gt;
&lt;li&gt;在某个输入框输入一些内容&lt;/li&gt;
&lt;li&gt;点击某个按钮&lt;/li&gt;
&lt;li&gt;检测某些文本&lt;/li&gt;
&lt;li&gt;判断是否处于某个界面 &lt;/li&gt;
&lt;li&gt;判断是否有弹出框，提示文本&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;而这些动作 (step)，cucumber 是可以用正则表达式进行标准化处理，在然后在各个测试用例中重用。&lt;/p&gt;
&lt;h3 id="改进邮箱登录故事AC需要用到的Step"&gt;改进邮箱登录故事 AC 需要用到的 Step&lt;/h3&gt;
&lt;hr&gt;

&lt;p&gt;$cat features/US004_login_by_email.feature&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;US_004&lt;/span&gt; &lt;span class="n"&gt;邮箱登录&lt;/span&gt;
  &lt;span class="n"&gt;为了正常使用需要登录身份的功能&lt;/span&gt;
  &lt;span class="n"&gt;作为一个已经用邮箱注册过的用户&lt;/span&gt;
  &lt;span class="n"&gt;我想要用邮箱和密码登录系统&lt;/span&gt;

  &lt;span class="vi"&gt;@reset_driver&lt;/span&gt;
  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_02&lt;/span&gt; &lt;span class="n"&gt;登录错误&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;正确邮箱&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;错误密码登录&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt; &lt;span class="n"&gt;点击&lt;/span&gt; &lt;span class="s2"&gt;"登录/注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"登录页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test_user@mytest.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"b123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"用户密码不匹配"&lt;/span&gt;
&lt;span class="o"&gt;.....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;$ cat features/step_definitions/steps.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我已经用邮箱 (.*) 与密码 (.*) 注册过账号$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# sleep(1)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: email: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: password: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# 等待主页面就绪, 主页面ID 为 home_page&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# 点击 主页面中的 '登录/注册' 按钮，按钮ID为 btn_to_login&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_to_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="c1"&gt;# 检查页面跳转到 登录页面， 登录页面ID为 page_login_account&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page_login_account'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "(.*?)" 输入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;input_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;input_field&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'邮箱或手机'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_username'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'密码'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_password'&lt;/span&gt;
               &lt;span class="k"&gt;else&lt;/span&gt;
                 &lt;span class="s1"&gt;'unknown'&lt;/span&gt;
             &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# 定位指定的输入框&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;                    &lt;span class="c1"&gt;# 清除原来的内容&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# 输入新内容并回车&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我按下按钮 "登录"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当到达 "主页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^等待 (\d+) 秒后.*/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;.&lt;/p&gt;
&lt;h3 id="When 我在 "&gt;When 我在 "主页面" 点击 "登录/注册" 进入 "登录页面"&lt;/h3&gt;
&lt;hr&gt;

&lt;p&gt;这个 step 的目的是我们要在某个界面，点击 某个组件 (按钮或链接），跳转到另一个页面。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# 等待主页面就绪, 主页面ID 为 home_page&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# 点击 主页面中的 '登录/注册' 按钮，按钮ID为 btn_to_login&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_to_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="c1"&gt;# 检查页面跳转到 登录页面， 登录页面ID为 page_login_account&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page_login_account'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面有三个小动作：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;确定当前是否在指定界面，是通过查找某个该界面中特有的组件 id 是否存在来判断。&lt;/li&gt;
&lt;li&gt;点击 某个元素，是通过查找到指定 id 的组件，向它发送 click 信号。&lt;/li&gt;
&lt;li&gt;判断当前界面是否是指定界面，同  1。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;因此每个界面，我们都设置一个能够标识该界面的一个唯一的 id，便于我们识别当前的界面。
其次每个需要测试交互的组件，都分配一个在该页面唯一的 id。
最后，为上面的硬编码 id 改为对照方式。step 改为正则匹配。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# When(/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/) do&lt;/span&gt;
&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "([^"]*)" 点击 "(.*?)" 进入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'主页面'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'密码登录页面'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;  &lt;span class="s1"&gt;'page_login_account'&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'登录/注册'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'btn_to_login'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;dest_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 steps 基本已经通用了。
只要 feature 中按照  &lt;code&gt;我在 "AAA" 点击 "BBB" 进入 "CCC"&lt;/code&gt; 这个格式写测试步骤，都能匹配处理。&lt;/p&gt;

&lt;p&gt;还有不足的的地方，每次新加按钮或页面 id 时，都需要进入该 step 中添加，而且，其他地方无法重用这些映射定义；另一个是 "=&amp;gt;" 这种映射写法，通不过 Rubocop 的语法检测。&lt;/p&gt;
&lt;h3 id="继续优化"&gt;继续优化&lt;/h3&gt;
&lt;hr&gt;

&lt;p&gt;把 VIEW_MAPPING BUTTON_MAPPING 移到一个新文件，作为全局的常量（其实 Ruby 中并没有真正的常量定义）。并且，反转映射写法以满足 Rubocop。
Cucumber 会自动加载 features/step_definitions 中所有的文件，无需自己手动 require.&lt;/p&gt;

&lt;p&gt;$ cat features/step_definitions/keyword_mapping.rb &lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;##&lt;/span&gt;
&lt;span class="c1"&gt;# 1. 按所在页面进行分类排序&lt;/span&gt;
&lt;span class="c1"&gt;# 2. 不同页面存在相同关键字（button或input), id应相同&lt;/span&gt;
&lt;span class="c1"&gt;# 3. 在下面注释中出现 '已被定义' 的前缀， 是为说明相同的关键字，在所处hash中已被定义，不需要重新定义&lt;/span&gt;

&lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;home_page: &lt;/span&gt;&lt;span class="s1"&gt;'主页面'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;page_more_races: &lt;/span&gt;&lt;span class="s1"&gt;'更多赛事页面'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;page_login_account: &lt;/span&gt;&lt;span class="s1"&gt;'密码登录页面'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;page_login_code: &lt;/span&gt;&lt;span class="s1"&gt;'验证码登录页面'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;....&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;invert&lt;/span&gt;

&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# 主页面&lt;/span&gt;
    &lt;span class="ss"&gt;btn_to_login: &lt;/span&gt;&lt;span class="s1"&gt;'登录/注册'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_races_1: &lt;/span&gt;&lt;span class="s1"&gt;'第一个赛事'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_race_detail: &lt;/span&gt;&lt;span class="s1"&gt;'赛事详情'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_order: &lt;/span&gt;&lt;span class="s1"&gt;'订单'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_setup: &lt;/span&gt;&lt;span class="s1"&gt;'设置'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_account_security: &lt;/span&gt;&lt;span class="s1"&gt;'账号安全'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_change_password: &lt;/span&gt;&lt;span class="s1"&gt;'修改密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_more_races: &lt;/span&gt;&lt;span class="s1"&gt;'更多赛事'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# 密码登录页面&lt;/span&gt;
    &lt;span class="ss"&gt;btn_bar_right: &lt;/span&gt;&lt;span class="s1"&gt;'注册'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;btn_bar_left: &lt;/span&gt;&lt;span class="s1"&gt;'左上'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;invert&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="改进界面输入的步骤，模版化"&gt;改进界面输入的步骤，模版化&lt;/h3&gt;
&lt;hr&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "(.*?)" 输入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;input_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;input_field&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'邮箱或手机'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_username'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'密码'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_password'&lt;/span&gt;
               &lt;span class="k"&gt;else&lt;/span&gt;
                 &lt;span class="s1"&gt;'unknown'&lt;/span&gt;
             &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# 定位指定的输入框&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;                    &lt;span class="c1"&gt;# 清除原来的内容&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# 输入新内容并回车&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于一开始我们就使用了正则匹配，仅是在 ID 这块做了条件硬编码，该为映射处理：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "(.*?)" 输入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;                &lt;span class="c1"&gt;# 定位指定的输入框&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;   &lt;span class="c1"&gt;# 输入新内容并回车&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                        &lt;span class="c1"&gt;# 输入完等待1秒，给模拟器留处理时间&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为 features/step_definitions/keyword_mapping.rb 添加 INPUT_MAPPING&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# 密码登录页面&lt;/span&gt;
    &lt;span class="ss"&gt;input_username: &lt;/span&gt;&lt;span class="s1"&gt;'邮箱或手机'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_password: &lt;/span&gt;&lt;span class="s1"&gt;'密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# 验证码登录页面&lt;/span&gt;
    &lt;span class="ss"&gt;input_phone: &lt;/span&gt;&lt;span class="s1"&gt;'手机号'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_code: &lt;/span&gt;&lt;span class="s1"&gt;'验证码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# 手机注册页面&lt;/span&gt;
    &lt;span class="c1"&gt;# 已被定义：手机号，验证码&lt;/span&gt;

    &lt;span class="c1"&gt;# 邮箱注册页面&lt;/span&gt;
    &lt;span class="ss"&gt;input_email: &lt;/span&gt;&lt;span class="s1"&gt;'邮箱'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# 实名认证&lt;/span&gt;
    &lt;span class="ss"&gt;input_real_name: &lt;/span&gt;&lt;span class="s1"&gt;'真实姓名'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_id_card: &lt;/span&gt;&lt;span class="s1"&gt;'身份证号'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;# 修改密码&lt;/span&gt;
    &lt;span class="ss"&gt;input_old_pwd: &lt;/span&gt;&lt;span class="s1"&gt;'旧密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_new_pwd: &lt;/span&gt;&lt;span class="s1"&gt;'新密码'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# 赛事&lt;/span&gt;
    &lt;span class="ss"&gt;input_keyword: &lt;/span&gt;&lt;span class="s1"&gt;'赛事关键字'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;invert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="继续改进剩余step"&gt;继续改进剩余 step&lt;/h3&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我按下按钮 "登录"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当到达 "主页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;==&amp;gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我[按下|点击]+按钮 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: '&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: got button: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当到达 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备注：Cucumber 的 Step 定义中，And Given Then When 这四个都是等价的语法糖，Then 定义的步骤，可以直接在其他步骤中使用。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这四种都是等价的。&lt;/p&gt;
&lt;h3 id="完整示例"&gt;完整示例&lt;/h3&gt;
&lt;hr&gt;

&lt;p&gt;当我们把常用的 Step 整理后，基本上已经能满足 95% 以上的测试用例编写，就连产品，设计也能愉快的按着写 AC 了
这个 step 定义，基本可以直接拿去使用，请叫我 &lt;strong&gt;红领巾&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;$ cat features/step_definitions/steps.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我已经用邮箱 (.*) 与密码 (.*) 注册过账号$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# sleep(1)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: email: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: password: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "([^"]*)" 点击 "(.*?)" 进入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;dest_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我点击 "([^"]*)" [进入|回到]+ "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;dest_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: got button: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dest_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我[按下|点击]+按钮 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: '&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: got button: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^点击原生button "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: got button: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "(.*?)" 输入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^等待 (\d+) 秒后.*/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当到达 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;VIEW_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/.*\(创建数据\)$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
  &lt;span class="n"&gt;ac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RemoteFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed_body&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我已使用 "([^"]*)" 登录$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RemoteFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ac_us001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed_body&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'回到主页'&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'回到主页'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'回到主页'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;to_login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'登录/注册'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'登录/注册'&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_login&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;email_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'邮箱或手机'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;password_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'密码'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;login&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'登录'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password_input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password_input&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'test123'&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^退出登录$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'回到主页'&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'回到主页'&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'回到主页'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;bar_left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'左上'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'左上'&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar_left&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;setup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'设置'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'设置'&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;btn_exit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'退出登录'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'退出登录'&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;btn_exit&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'确定'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^清除数据$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RemoteFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'clear'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parsed_body&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到 "(.*?)" 显示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;location_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;INPUT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^"([^"]*)" 按钮置灰，无法点击$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button_text&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^"([^"]*)" 按钮无法点击$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button_text&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^看到的 "([^"]*)" 应为 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;text_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;TEXT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;text_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;text_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;target_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;target_value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; eql? &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;target_value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eql?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我能看到 "([^"]*)" 这些元素$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;element&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TEXT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在Alert中点击 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button_text&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIAAlert'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^上传图片$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_picker_image'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'图库'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'好'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Camera Roll'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'XCUIElementTypeCell'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^隐藏键盘$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;hide_keyboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Return'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^打印调试 (.+)$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;debug_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;debug_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;debug_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'page'&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt;
  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;debug_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'source'&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^"([^"]*)" 应隐藏/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button_text&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我能看到 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^上滑$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# swipe start_x: 300, start_y: 300, offest_x: 0, offset_y: -200&lt;/span&gt;
  &lt;span class="n"&gt;swipe&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="s1"&gt;'up'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^下拉刷新$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# swipe start_x: 300, start_y: 300, offest_x: 0, offset_y: 200&lt;/span&gt;
  &lt;span class="n"&gt;swipe&lt;/span&gt; &lt;span class="ss"&gt;direction: &lt;/span&gt;&lt;span class="s1"&gt;'down'&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应该找不到 "([^"]*)" 这些元素$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"存在&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;这个元素"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;TEXT_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^应不存在 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"存在&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^在 "([^"]*)" 可匹配到 "([^"]*)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;button_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BUTTON_MAPPING&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;button_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: got label: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="s2"&gt;"未匹配到&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^pending:.*$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;pending&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="完整用户故事例子"&gt;完整用户故事例子&lt;/h3&gt;&lt;h4 id="US_001 邮箱注册"&gt;US_001 邮箱注册&lt;/h4&gt;
&lt;hr&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;US_001&lt;/span&gt; &lt;span class="n"&gt;邮箱注册&lt;/span&gt;
  &lt;span class="n"&gt;作为一名非注册用户&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;我需要用邮箱号&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;使得我可以完成注册&lt;/span&gt;

  &lt;span class="vi"&gt;@reset_driver&lt;/span&gt;
  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_01&lt;/span&gt; &lt;span class="n"&gt;注册错误&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;错误邮箱注册&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt; &lt;span class="n"&gt;点击&lt;/span&gt; &lt;span class="s2"&gt;"登录/注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"密码登录页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"手机注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"使用邮箱注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"邮箱注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"aa@desh"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test123"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"您的电子邮件格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_02&lt;/span&gt; &lt;span class="n"&gt;下一步按钮是灰色状态&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt; &lt;span class="n"&gt;按钮置灰&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;无法点击&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_03&lt;/span&gt; &lt;span class="n"&gt;成功跳转到&lt;/span&gt; &lt;span class="n"&gt;手机注册页面&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"使用手机注册"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"手机注册页面"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_04&lt;/span&gt; &lt;span class="n"&gt;成功跳转到&lt;/span&gt; &lt;span class="n"&gt;密码登录页面&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"使用邮箱注册"&lt;/span&gt; &lt;span class="n"&gt;回到&lt;/span&gt; &lt;span class="s2"&gt;"邮箱注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"我已有账号"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"密码登录页面"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_05&lt;/span&gt; &lt;span class="n"&gt;注册错误&lt;/span&gt; &lt;span class="n"&gt;邮箱已注册&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;创建数据&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ac&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;clear&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;AC_US001&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@gmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"手机注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"使用邮箱注册"&lt;/span&gt; &lt;span class="n"&gt;回到&lt;/span&gt; &lt;span class="s2"&gt;"邮箱注册页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test@gmail.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test123"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"邮箱已被使用"&lt;/span&gt;

&lt;span class="c1"&gt;#  Scenario: AC_US001_06  备注：重复ac,与AC_US001_01重复&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_07&lt;/span&gt; &lt;span class="n"&gt;注册错误&lt;/span&gt; &lt;span class="n"&gt;密码格式错误&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test@gmail.com"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"密码格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US001_08&lt;/span&gt; &lt;span class="n"&gt;注册成功&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test1@gmail.com"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="US-006 忘记密码-邮箱找回"&gt;US-006 忘记密码 - 邮箱找回&lt;/h4&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;US&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;006&lt;/span&gt; &lt;span class="n"&gt;忘记密码&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;邮箱找回&lt;/span&gt;
  &lt;span class="n"&gt;作为一名忘记密码的用户&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;我需要用已认证的邮箱&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;使得我能够找回密码&lt;/span&gt;

  &lt;span class="vi"&gt;@reset_driver&lt;/span&gt;
  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;01&lt;/span&gt; &lt;span class="n"&gt;没有任何输入&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt; &lt;span class="n"&gt;点击&lt;/span&gt; &lt;span class="s2"&gt;"登录/注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"密码登录页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"忘记密码"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"忘记密码页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"使用邮箱找回密码"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="s2"&gt;"下一步"&lt;/span&gt; &lt;span class="n"&gt;按钮置灰&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;无法点击&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;02&lt;/span&gt; &lt;span class="n"&gt;没有任何输入&lt;/span&gt; &lt;span class="n"&gt;点击获取验证码&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"您的电子邮件格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;03&lt;/span&gt; &lt;span class="n"&gt;错误格式的邮箱&lt;/span&gt; &lt;span class="n"&gt;点击获取验证码&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test@aa"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"您的电子邮件格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;04&lt;/span&gt; &lt;span class="n"&gt;错误格式的邮箱&lt;/span&gt; &lt;span class="n"&gt;点击获取验证码&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"ricky@aa"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"您的电子邮件格式不正确"&lt;/span&gt;

&lt;span class="c1"&gt;#  Scenario: AC_US006_05  备注：重复ac,与AC_US006_07重复&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;06&lt;/span&gt; &lt;span class="n"&gt;未输入邮箱&lt;/span&gt; &lt;span class="n"&gt;但输入了验证码&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"验证码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"aaa"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"您的电子邮件格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mo"&gt;07&lt;/span&gt; &lt;span class="n"&gt;输入正确的邮箱&lt;/span&gt; &lt;span class="n"&gt;及正确的验证码&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@aa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;创建数据&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ac&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;AC_US006_07&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@aa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test@aa.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"验证码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;隐藏键盘&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"下一步"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"输入密码页面"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt; &lt;span class="n"&gt;输入正确的邮箱&lt;/span&gt; &lt;span class="n"&gt;及正确的验证码&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@aa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;创建数据&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;ac&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="no"&gt;AC_US006_08&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="vi"&gt;@aa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"密码格式不正确"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;US006&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt; &lt;span class="n"&gt;输入正确的邮箱&lt;/span&gt; &lt;span class="n"&gt;及正确的验证码&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"回到主页"&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt; &lt;span class="n"&gt;点击&lt;/span&gt; &lt;span class="s2"&gt;"登录/注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"密码登录页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我点击&lt;/span&gt; &lt;span class="s2"&gt;"忘记密码"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"忘记密码页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"使用邮箱找回密码"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test@aa.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"获取验证码"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"验证码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"下一步"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"a123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"完成"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"密码登录页面"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id="最后，为了确保测试用例每次都能正确，我们专门部署一个后端持续集成版本，额外提供了一组用于重置/创建测试用例数据，用于支持APP测试。"&gt;最后，为了确保测试用例每次都能正确，我们专门部署一个后端持续集成版本，额外提供了一组用于重置/创建测试用例数据，用于支持 APP 测试。&lt;/h3&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Mon, 07 Aug 2017 09:27:49 +0800</pubDate>
      <link>https://ruby-china.org/topics/33749</link>
      <guid>https://ruby-china.org/topics/33749</guid>
    </item>
    <item>
      <title>敏捷实践 (3) - Appium Ruby Console</title>
      <description>&lt;p&gt;&lt;a href="http://www.jianshu.com/p/10164589e694" rel="nofollow" target="_blank"&gt;http://www.jianshu.com/p/10164589e694&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-5ada0515e95934a5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="appium.png"&gt;&lt;/p&gt;
&lt;h2 id="1. Appium Ruby Console"&gt;1. Appium Ruby Console&lt;/h2&gt;
&lt;p&gt;Appium Ruby Console (ARC) 顾名思义就是 Appium 控制台（命令行模式）。
&lt;a href="https://github.com/appium/ruby_console" rel="nofollow" target="_blank"&gt;https://github.com/appium/ruby_console&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="2. 安装"&gt;2. 安装&lt;/h2&gt;
&lt;p&gt;如果你在 &lt;a href="http://www.jianshu.com/p/fd17c1bb6806" rel="nofollow" target="_blank" title=""&gt;敏捷实践 (1) - 我们是如何自动化 App 验收标准&lt;/a&gt; 中，已经安装了 ARC，这个步就可以忽略。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gem install appium_lib
$ gem install appium_console
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="3. 启动"&gt;3. 启动&lt;/h2&gt;&lt;h3 id="3.1 appium"&gt;3.1 appium&lt;/h3&gt;
&lt;p&gt;在打开一个命令行窗口运行 appium 服务
&lt;code&gt;$ appium&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-24eac1860aedb186.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="run_appium.png"&gt;&lt;/p&gt;
&lt;h3 id="3.2 拉取Sample-Code"&gt;3.2 拉取 Sample-Code&lt;/h3&gt;
&lt;p&gt;`$ git clone &lt;a href="https://github.com/appium/sample-code.git" rel="nofollow" target="_blank"&gt;https://github.com/appium/sample-code.git&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="3.3 运行ARC"&gt;3.3 运行 ARC&lt;/h3&gt;
&lt;p&gt;arc 会自动读取 appium.txt 并通知 appium 启动 appium.txt 指定的 App&lt;/p&gt;

&lt;p&gt;app 的位置在 sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ cd sample-code/sample-code/examples/ruby
# 运行 bundle 安装依赖包
$ bundle
$ arc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个一步发生了什么？
** ARC 启动，读取 appium.txt 的配置发送给 appium **&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-5931f1c692953cd9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="run_arc.png"&gt;&lt;/p&gt;

&lt;p&gt;** appium 收到请求，启动模拟器，安装 WebDriverAgent &amp;amp; TestApp **
下面粘贴的日志信息可以让大家看到许多背后的细节，对理解 appium 如何工作很有帮助。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EdwarddeMBP:appium_work edwardzhou$ appium
[Appium] Welcome to Appium v1.6.3
[Appium] Appium REST http interface listener started on 0.0.0.0:4723
[HTTP] --&amp;gt; POST /wd/hub/session {"desiredCapabilities":{"browserName":"","version":"","platform":"ANY","javascriptEnabled":true,"cssSelectorsEnabled":true,"takesScreenshot":true,"nativeEvents":false,"rotatable":false,"newCommandTimeout":999999,"platformName":"ios","versionNumber":"10.2","deviceName":"iPhone 6","app":"/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app","automationName":"XCUITest","language":"zh","locale":"zh_CN","autoAcceptAlerts":true}}
[debug] [MJSONWP] Calling AppiumDriver.createSession() with args: [{"browserName":"","version":"","platform":"ANY","javascriptEnabled":true,"cssSelectorsEnabled":true,"takesScreenshot":true,"nativeEvents":false,"rotatable":false,"newCommandTimeout":999999,"platformName":"ios","versionNumber":"10.2","deviceName":"iPhone 6","app":"/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app","automationName":"XCUITest","language":"zh","locale":"zh_CN","autoAcceptAlerts":true},null,null,null,null]
[Appium] Creating new XCUITestDriver session
[Appium] Capabilities:
[Appium]   browserName: ''
[Appium]   version: ''
[Appium]   platform: 'ANY'
[Appium]   javascriptEnabled: true
[Appium]   cssSelectorsEnabled: true
[Appium]   takesScreenshot: true
[Appium]   nativeEvents: false
[Appium]   rotatable: false
[Appium]   newCommandTimeout: 999999
[Appium]   platformName: 'ios'
[Appium]   versionNumber: '10.2'
[Appium]   deviceName: 'iPhone 6'
[Appium]   app: '/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app'
[Appium]   automationName: 'XCUITest'
[Appium]   language: 'zh'
[Appium]   locale: 'zh_CN'
[Appium]   autoAcceptAlerts: true
[debug] [XCUITest] XCUITestDriver version: 2.5.3
[BaseDriver] The following capabilities were provided, but are not recognized by appium: version, platform, javascriptEnabled, cssSelectorsEnabled, takesScreenshot, nativeEvents, rotatable, versionNumber.
[XCUITest] The capabilities 'autoAcceptAlerts' and 'autoDismissAlerts' do not work for XCUITest-based tests. Please adjust your alert handling accordingly.
[BaseDriver] Session created with session id: 136bbab8-78e5-4e5d-9cd1-0b174eeaa835
[debug] [XCUITest] Xcode version set to '8.2.1'
[debug] [XCUITest] iOS SDK Version set to '10.2'
[XCUITest] Simluator udid not provided, using desired caps to create a new simulator
[XCUITest] No platformVersion specified. Using latest version Xcode supports: '10.2' This may cause problems if a simulator does not exist for this platform version.
[iOSSim] Constructing iOS simulator for Xcode version 8.2.1 with udid 'C2E88820-BB65-4D79-923B-15C63D5FA384'
[XCUITest] Created simulator with udid 'C2E88820-BB65-4D79-923B-15C63D5FA384'.
[XCUITest] Determining device to run tests on: udid: 'C2E88820-BB65-4D79-923B-15C63D5FA384', real device: false
[BaseDriver] Using local app '/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app'
[debug] [XCUITest] Checking whether app '/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app' is actually present
[debug] [XCUITest] App is present
[debug] [iOS] Getting bundle ID from app '/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app': 'io.appium.TestApp'
[debug] [iOSLog] Starting iOS 10.2 simulator log capture
[debug] [iOSLog] System log path: /Users/edwardzhou/Library/Logs/CoreSimulator/C2E88820-BB65-4D79-923B-15C63D5FA384/system.log
[XCUITest] Setting up simulator
[debug] [iOSSim] Checking whether simulator has been run before
[debug] [iOSSim] Simulator has not been run before
[debug] [iOS] No simulator directories found.
[debug] [iOSSim] Attempting to launch and quit the simulator, to create directory structure
[debug] [iOSSim] Will launch with Safari? false
[iOSSim] Starting simulator with command: open -Fn /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID C2E88820-BB65-4D79-923B-15C63D5FA384
[iOSSim] Simulator log at '/Users/edwardzhou/Library/Logs/CoreSimulator/C2E88820-BB65-4D79-923B-15C63D5FA384/system.log'
[iOSSim] Tailing simulator logs until we encounter the string "com.apple.springboard"
[iOSSim] We will time out after 60000ms
[debug] [iOSSim] Waiting an extra 10000ms for the simulator to really finish booting
[debug] [iOSSim] Done waiting extra time for simulator
[iOSSim] Simulator booted in 11143ms
[debug] [iOSSim] Checking whether simulator has been run before
[debug] [iOSSim] Simulator has been run before
[debug] [iOSSim] Killing all iOS Simulators
[debug] [iOS] Setting locale information
[debug] [iOSSim] New language: zh
[debug] [iOSSim] New locale: zh_CN
[debug] [iOSSim] Writing new locale plist data
[debug] [iOS] Locale was updated. Stopping simulator.
[debug] [iOS] Killing the simulator
[debug] [iOSSim] Killing all iOS Simulators
[debug] [iOS] No iOS / app preferences to set
[XCUITest] Simulator with udid 'C2E88820-BB65-4D79-923B-15C63D5FA384' not booted. Booting up now
[debug] [iOSSim] Killing all iOS Simulators
[iOSSim] Starting simulator with command: open -Fn /Applications/Xcode.app/Contents/Developer/Applications/Simulator.app --args -CurrentDeviceUDID C2E88820-BB65-4D79-923B-15C63D5FA384
[iOSSim] Simulator log at '/Users/edwardzhou/Library/Logs/CoreSimulator/C2E88820-BB65-4D79-923B-15C63D5FA384/system.log'
[iOSSim] Tailing simulator logs until we encounter the string "com.apple.springboard"
[iOSSim] We will time out after 60000ms
[debug] [iOSSim] Waiting an extra 10000ms for the simulator to really finish booting
[debug] [iOSSim] Done waiting extra time for simulator
[iOSSim] Simulator booted in 22072ms
[debug] [XCUITest] Installing app '/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app' on device
[XCUITest] Using WDA path: '/Users/edwardzhou/servers/node-v7.4.0/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent'
[XCUITest] Using WDA agent: '/Users/edwardzhou/servers/node-v7.4.0/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent/WebDriverAgent.xcodeproj'
[XCUITest] Launching WebDriverAgent on the device
[debug] [XCUITest] Carthage found: /usr/local/bin/carthage
[debug] [XCUITest] Killing hanging processes
[debug] [XCUITest] Beginning test with command 'xcodebuild build-for-testing test-without-building -project /Users/edwardzhou/servers/node-v7.4.0/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent/WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination id=C2E88820-BB65-4D79-923B-15C63D5FA384 -configuration Debug' in directory '/Users/edwardzhou/servers/node-v7.4.0/lib/node_modules/appium/node_modules/appium-xcuitest-driver/WebDriverAgent'
[debug] [XCUITest] Waiting up to 60000ms for WebDriverAgent to start
[debug] [XCUITest] Log file for xcodebuild test: /Users/edwardzhou/Library/Developer/Xcode/DerivedData/WebDriverAgent-ddapapcqxaoabrhjkjsyvzexgoxy/Logs/Test/98A1C0F5-03F0-4A95-9408-FE68E30628B2/Session-WebDriverAgentRunner-2017-02-26_191404-oFrhPq.log
[debug] [XCUITest] WebDriverAgent successfully started after 15361ms
[debug] [XCUITest] Sending createSession command to WDA
[debug] [JSONWP Proxy] Proxying [POST /session] to [POST http://localhost:8100/session] with body: {"desiredCapabilities":{"bundleId":"io.appium.TestApp","arguments":[],"environment":{},"shouldWaitForQuiescence":true}}
[debug] [JSONWP Proxy] Got response with status 200: {"value":{"sessionId":"AC8C1ADF-3E96-4C55-9302-73B4D7AA6B89","capabilities":{"device":"iphone","browserName":" ","sdkVersion":"10.2","CFBundleIdentifier":"local.pid.35906"}},"sessionId":"AC8C1ADF-3E96-4C55-9302-73B4D7AA6B89","status":0}
[debug] [XCUITest] Setting initial orientation to 'PORTRAIT'
[debug] [JSONWP Proxy] Proxying [POST /orientation] to [POST http://localhost:8100/session/AC8C1ADF-3E96-4C55-9302-73B4D7AA6B89/orientation] with body: {"orientation":"PORTRAIT"}
[debug] [JSONWP Proxy] Got response with status 200: {"value":{},"sessionId":"AC8C1ADF-3E96-4C55-9302-73B4D7AA6B89","status":0}
[Appium] New XCUITestDriver session created successfully, session 136bbab8-78e5-4e5d-9cd1-0b174eeaa835 added to master session list
[debug] [MJSONWP] Responding to client with driver.createSession() result: {"webStorageEnabled":false,"locationContextEnabled":false,"browserName":"","platform":"ANY","javascriptEnabled":true,"databaseEnabled":false,"takesScreenshot":true,"networkConnectionEnabled":false,"version":"","cssSelectorsEnabled":true,"nativeEvents":false,"rotatable":false,"newCommandTimeout":999999,"platformName":"ios","versionNumber":"10.2","deviceName":"iPhone 6","app":"/Users/edwardzhou/appium_work/sample-code/sample-code/apps/TestApp/build/release-iphonesimulator/TestApp.app","automationName":"XCUITest","language":"zh","locale":"zh_CN","autoAcceptAlerts":true}
[HTTP] &amp;lt;-- POST /wd/hub/session 200 68905 ms - 645 
[HTTP] --&amp;gt; GET /wd/hub/status {}
[debug] [MJSONWP] Calling AppiumDriver.getStatus() with args: []
[debug] [MJSONWP] Responding to client with driver.getStatus() result: {"build":{"version":"1.6.3","revision":null}}
[HTTP] &amp;lt;-- GET /wd/hub/status 200 11 ms - 83 
[HTTP] --&amp;gt; POST /wd/hub/session/136bbab8-78e5-4e5d-9cd1-0b174eeaa835/timeouts/implicit_wait {"ms":0}
[debug] [MJSONWP] Calling AppiumDriver.implicitWait() with args: [0,"136bbab8-78e5-4e5d-9cd1-0b174eeaa835"]
[debug] [XCUITest] Executing command 'implicitWait'
[debug] [BaseDriver] Set implicit wait to 0ms
[debug] [MJSONWP] Responding to client with driver.implicitWait() result: null
[HTTP] &amp;lt;-- POST /wd/hub/session/136bbab8-78e5-4e5d-9cd1-0b174eeaa835/timeouts/implicit_wait 200 17 ms - 76 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;** iOS 模拟器就绪**&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-dabd1a8cf00b1ffc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="ios_01.png"&gt;&lt;/p&gt;

&lt;p&gt;大家看到，app 首次运行弹出的 alert，就是导致 sample-code 中的测试用例无法通过的原因。&lt;/p&gt;

&lt;p&gt;因为，所有的查询定位，都是在当前视图中处理，这个 alert 出现后就改变了当前视图。&lt;/p&gt;

&lt;p&gt;let me show you~&lt;/p&gt;

&lt;p&gt;在 arc 中输入 page，回车&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeApplication&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label:  
&lt;/span&gt;&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;格&lt;/span&gt; &lt;span class="no"&gt;Wi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;Fi&lt;/span&gt; &lt;span class="n"&gt;信号&lt;/span&gt;&lt;span class="err"&gt;（&lt;/span&gt;&lt;span class="n"&gt;共&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;格&lt;/span&gt;&lt;span class="err"&gt;）&lt;/span&gt;
   &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="no"&gt;SSID&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;下午7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;电池电量&lt;/span&gt;&lt;span class="err"&gt;：&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeAlert&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="no"&gt;TestApp&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;可能使&lt;/span&gt; &lt;span class="n"&gt;iPhone&lt;/span&gt; &lt;span class="n"&gt;变慢&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="no"&gt;TestApp&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="n"&gt;可能使&lt;/span&gt; &lt;span class="n"&gt;iPhone&lt;/span&gt; &lt;span class="n"&gt;变慢&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;应用开发者需要更新此应用以改进其兼容性&lt;/span&gt;&lt;span class="err"&gt;。&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;好&lt;/span&gt;
&lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;page&lt;/strong&gt; 指令背后的细节 (appium log)&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/wd/&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;136&lt;/span&gt;&lt;span class="n"&gt;bbab8&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;78e5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4e5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;cd1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mb"&gt;0b1&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="n"&gt;eeaa835&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;MJSONWP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Calling&lt;/span&gt; &lt;span class="no"&gt;AppiumDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="ss"&gt;args: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"136bbab8-78e5-4e5d-9cd1-0b174eeaa835"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;XCUITest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Executing&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'getCurrentContext'&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;MJSONWP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Responding&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="s2"&gt;"NATIVE_APP"&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;--&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/wd/&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;136&lt;/span&gt;&lt;span class="n"&gt;bbab8&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;78e5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4e5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;cd1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mb"&gt;0b1&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="n"&gt;eeaa835&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt; 
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/wd/&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;136&lt;/span&gt;&lt;span class="n"&gt;bbab8&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;78e5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4e5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;cd1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mb"&gt;0b1&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="n"&gt;eeaa835&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;MJSONWP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Calling&lt;/span&gt; &lt;span class="no"&gt;AppiumDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="ss"&gt;args: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"136bbab8-78e5-4e5d-9cd1-0b174eeaa835"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;XCUITest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Executing&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'getCurrentContext'&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;MJSONWP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Responding&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ss"&gt;result: &lt;/span&gt;&lt;span class="s2"&gt;"NATIVE_APP"&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;--&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/wd/&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;136&lt;/span&gt;&lt;span class="n"&gt;bbab8&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;78e5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4e5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;cd1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mb"&gt;0b1&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="n"&gt;eeaa835&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;84&lt;/span&gt; 
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/wd/&lt;/span&gt;&lt;span class="n"&gt;hub&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;136&lt;/span&gt;&lt;span class="n"&gt;bbab8&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;78e5&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;4e5&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="n"&gt;cd1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mb"&gt;0b1&lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="n"&gt;eeaa835&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;MJSONWP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Calling&lt;/span&gt; &lt;span class="no"&gt;AppiumDriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="ss"&gt;args: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"136bbab8-78e5-4e5d-9cd1-0b174eeaa835"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;XCUITest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Executing&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="s1"&gt;'getPageSource'&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;JSONWP&lt;/span&gt; &lt;span class="no"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Proxying&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="sr"&gt;/source/xm&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="ss"&gt;:/&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8100&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="no"&gt;AC8C1ADF&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3E96&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="no"&gt;C55&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9302&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;73&lt;/span&gt;&lt;span class="no"&gt;B4D7AA6B89&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;JSONWP&lt;/span&gt; &lt;span class="no"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="no"&gt;Got&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; : {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;tree&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; : &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;?xml version=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;1.0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; encoding=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;UTF-8&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;?&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n&amp;lt;XCUIElementTypeApplication type=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeApplication&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; name=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; label=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  &amp;lt;XCUIElementTypeWindow type=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeWindow&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n    &amp;lt;XCUIElementTypeOther type=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeOther&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\\\"\\&lt;/span&gt;&lt;span class="s2"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n    &amp;lt;XCUIElementTypeOther type=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeOther&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\\\"\\&lt;/span&gt;&lt;span class="s2"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  &amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;/XCUIElementTypeWindow&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  &amp;lt;XCUIElementTypeWindow type=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeWindow&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n    &amp;lt;XCUIElementType...
[debug] [MJSONWP] Responding to client with driver.getPageSource() result: "&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;"1.0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; encoding=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;UTF-8&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;?&amp;gt;&amp;lt;AppiumAUT&amp;gt;&amp;lt;XCUIElementTypeApplication type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeApplication&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; name=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; label=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;XCUIElementTypeWindow type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeWindow&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    &amp;lt;XCUIElementTypeOther type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeOther&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    &amp;lt;XCUIElementTypeOther type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeOther&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;/XCUIElementTypeWindow&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;XCUIElementTypeWindow type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeWindow&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;    &amp;lt;XCUIElementTypeOther type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeOther&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; visible=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; enabled=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; x=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; y=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; width=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;375&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; height=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;667&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;/XCUIElementTypeWindow&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  &amp;lt;XCUIElementTypeWindow type=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;XCUIElementTypeWin...
[HTTP] &amp;lt;-- GET /wd/hub/session/136bbab8-78e5-4e5d-9cd1-0b174eeaa835/source 200 422 ms - 14307 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击“好”，或直接在 arc 中输入 &lt;strong&gt;alert_accept&lt;/strong&gt; 回车，关闭对话框&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-0f3dd827ce756174.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="ios_02.png"&gt;&lt;/p&gt;
&lt;h2 id="4. 测试指令"&gt;4. 测试指令&lt;/h2&gt;&lt;h3 id="4.1 page"&gt;4.1 page&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;page&lt;/strong&gt; 用于查看当前视图中有哪些元素，并以行文本形式输出&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;alert_accept&lt;/span&gt;
&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeApplication&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;TestApp&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeTextField&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;IntegerA&lt;/span&gt;
  &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;TextField1&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeTextField&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;IntegerB&lt;/span&gt;
  &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;TextField2&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;ComputeSumButton&lt;/span&gt;
  &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;Compute&lt;/span&gt; &lt;span class="no"&gt;Sum&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;Answer&lt;/span&gt;
  &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;SumLabel&lt;/span&gt;
  &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="no"&gt;SumLabel&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;contact&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="no"&gt;AppElem&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeSlider&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;AppElem&lt;/span&gt;
   &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;Accessibility&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeStaticText&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="no"&gt;AppElem&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;DisabledButton&lt;/span&gt;
  &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;disabled&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeSwitch&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;locationStatus&lt;/span&gt;
   &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt; &lt;span class="no"&gt;Gesture&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeButton&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="no"&gt;Crash&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="no"&gt;Wi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="no"&gt;Fi&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;
   &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="no"&gt;SSID&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="n"&gt;下午7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;
&lt;span class="no"&gt;XCUIElementTypeOther&lt;/span&gt;
   &lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;label: &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;battery&lt;/span&gt; &lt;span class="n"&gt;power&lt;/span&gt;
&lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;pry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试应用为 TestApp&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;XCUIElementTypeApplication
   name, label: TestApp&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;第一个输入框，类型为 XCUIElementTypeTextField, 
name (id) 为 IntegerA
文本标签 TextField1&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;XCUIElementTypeTextField
   name: IntegerA
  label: TextField1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;....&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="4.2 source"&gt;4.2 source&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;source&lt;/strong&gt; 作用同 &lt;strong&gt;Page&lt;/strong&gt;
source 返回个是 XML 的文档格式。&lt;/p&gt;
&lt;h3 id="4.3 查找元素(find_element, find, id, xpath ...)"&gt;4.3 查找元素 (find_element, find, id, xpath ...)&lt;/h3&gt;
&lt;p&gt;Ref:
&lt;a href="https://github.com/appium/ruby_lib/blob/master/docs/docs.md" rel="nofollow" target="_blank"&gt;https://github.com/appium/ruby_lib/blob/master/docs/docs.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Appium Ruby Lib（SeleniumDriver）真正提供的查找元素的方法是&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;find_element - 查找单个元素，如果有多个满足条件的，返回第一个。如果一个都没有，报错。&lt;/li&gt;
&lt;li&gt;find_elements - 查找多个元素，并以数组形式返回。没找到，返回空数组 []。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其他的方法（find, id, xpath ...)，都是语法糖。&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;语法糖&lt;/th&gt;
&lt;th&gt;对应 find_element(s)&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id('comp_id')&lt;/td&gt;
&lt;td&gt;find_element(:id, 'comp_id')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;buttons&lt;/td&gt;
&lt;td&gt;find_elements(:class, 'XCUIElementTypeButton')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;button(index)&lt;/td&gt;
&lt;td&gt;=&amp;gt; buttons[index]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;texts&lt;/td&gt;
&lt;td&gt;find_elements(:class, 'XCUIElementTypeStaticText')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text(index)&lt;/td&gt;
&lt;td&gt;=&amp;gt; texts[index]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;textfields&lt;/td&gt;
&lt;td&gt;find_elements(:class, 'XCUIElementTypeTextField')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;textfield[index]&lt;/td&gt;
&lt;td&gt;=&amp;gt; textfields[index]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xpath('xpath_expr')&lt;/td&gt;
&lt;td&gt;find_element(:xpath, 'xpath_expr')&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xpaths('xpath_expr')&lt;/td&gt;
&lt;td&gt;find_elements(:xpath, 'xpath_expr)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;finds('text')&lt;/td&gt;
&lt;td&gt;find_elements(:xpath, "(//*)[@*[contains(translate(., \"Text\", \"text\"), \"text\")]]"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;find('text')&lt;/td&gt;
&lt;td&gt;=&amp;gt; finds('text')[0]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;基于性能考虑，建议尽量使用 id 查找元素，find/finds 的性能最差。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="4.4 对元素执行动作"&gt;4.4 对元素执行动作&lt;/h3&gt;
&lt;p&gt;拿到一个元素 e 之后，我们就是直接访问属性或发送动作。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="c1"&gt;# button, text&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="c1"&gt;# secure, textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'some text'&lt;/span&gt; &lt;span class="c1"&gt;# type text into textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt; &lt;span class="c1"&gt;# clear textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag_name&lt;/span&gt; &lt;span class="c1"&gt;# calls .type (patch.rb)&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;location&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rel_location&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt; &lt;span class="s1"&gt;'keys to send'&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_value&lt;/span&gt; &lt;span class="s1"&gt;'value to set'&lt;/span&gt; &lt;span class="c1"&gt;# ruby_console specific&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayed?&lt;/span&gt; &lt;span class="c1"&gt;# true or false depending if the element is visible&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selected?&lt;/span&gt; &lt;span class="c1"&gt;# is the tab selected?&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'checked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# is the checkbox checked?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些属性和动作的背后，都是发送指令到 appium，可以直接在 appium 运行日志中看到每个请求的具体细节。&lt;/p&gt;
&lt;h4 id="有一点需要注意的是，e.click 背后的实现是，先获取 e 的屏幕坐标，然后向该坐标发送触碰事件，并不是说能够直接控制e触发点击事件。因此，如果e被其他控件（如键盘）遮住，则触碰事件实际是其他控件接受了。 这个着实坑了我们一把。"&gt;有一点需要注意的是，e.click 背后的实现是，先获取 e 的屏幕坐标，然后向该坐标发送触碰事件，并不是说能够直接控制 e 触发点击事件。因此，如果 e 被其他控件（如键盘）遮住，则触碰事件实际是其他控件接受了。这个着实坑了我们一把。&lt;/h4&gt;
&lt;hr&gt;

&lt;p&gt;以后有时间再接着第一篇说说我们是如何改进测试用例，把 steps 模版化。&lt;/p&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Mon, 07 Aug 2017 09:05:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/33748</link>
      <guid>https://ruby-china.org/topics/33748</guid>
    </item>
    <item>
      <title>敏捷实践 (2) - appium 支持与无法支持的测试</title>
      <description>&lt;p&gt;&lt;a href="http://www.jianshu.com/p/c3f0d0684eef" rel="nofollow" target="_blank"&gt;http://www.jianshu.com/p/c3f0d0684eef&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="1. 简介"&gt;1. 简介&lt;/h2&gt;
&lt;p&gt;在探索 App 自动化测试工具过程中，主要接触了 &lt;a href="https://macacajs.github.io/zh/introduction" rel="nofollow" target="_blank" title=""&gt;Macaca&lt;/a&gt; 和 &lt;a href="http://appium.io/" rel="nofollow" target="_blank" title=""&gt;Appium&lt;/a&gt;, 已及稍稍看了点 &lt;a href="http://calaba.sh/" rel="nofollow" target="_blank" title=""&gt;Calabash&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;其中，Calabash 与 Appium 都支持使用 &lt;a href="https://docs.cucumber.io/" rel="nofollow" target="_blank" title=""&gt;Cucumber&lt;/a&gt; 编写测试用例。由于精力有限，Calabash 没有更进一步研究，有兴趣的朋友可以看看，交流一下心得。&lt;/p&gt;

&lt;p&gt;Macaca 在 &lt;a href="http://www.jianshu.com/p/fd17c1bb6806" rel="nofollow" target="_blank" title=""&gt;敏捷实践 - 我们是如何自动化 App 验收标准（一）&lt;/a&gt; 一文中有提到过，是我的前端小伙伴 Lorne 向我推荐的，看了文档，也装上试了试，不得不说 Macaca 非常优秀，上手非常容易，同时支持 NodeJS 与 Python 与 Java 三大主流语言。就是文档稍有些少。推荐大家试试。&lt;/p&gt;

&lt;p&gt;Appium 是个非常优秀的 App 自动化测试工具，支持的语言比 Macaca 多一些。让我选择 Appium 而不是 Macaca 的关键因数并非所支持语言的多少，而是 Appium 支持用 Ruby+Cucumber 来写测试用例，简直就像为我们量身定造。没错，我最喜欢用的语言首选 Ruby，其次 NodeJS/Python，再之 Java。&lt;/p&gt;

&lt;p&gt;我们的测试用例起步于 &lt;a href="https://github.com/appium/sample-code/" rel="nofollow" target="_blank" title=""&gt;appium sample-code&lt;/a&gt; 中的 ruby 部分，强烈推荐大家去翻翻代码。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. Appium"&gt;2. Appium&lt;/h2&gt;
&lt;p&gt;Appium 的详细介绍信息可以参考 &lt;a href="http://appium.io/slate/en/master/?ruby#about-appium" rel="nofollow" target="_blank" title=""&gt;在线文档&lt;/a&gt;，在这里我并不打算太多重复，而且网上也有不少教程了，只想概要的说明一下它是如何工作的。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="2.1 Appium的体系结构"&gt;2.1 Appium 的体系结构&lt;/h3&gt;
&lt;p&gt;Appium 是用 NodeJS 写的一个 HTTP 服务，因此使用它需要先安装 NodeJS。它又是通过 WebDriverAgent 组件在移动设备中控制被测试应用执行测试指令。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="2.2 工作流程图"&gt;2.2 工作流程图&lt;/h3&gt;
&lt;p&gt;注意，这个流程图并非官方图，只是我随手画的帮助大家理解的示意图。&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-b66f251907c4c176.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="appium_flow.png"&gt;&lt;/p&gt;

&lt;p&gt;测试脚本与 Appium 与被测试的 APP 是分别各自独立的进程，甚至分署在不同的主机与真实设备中。&lt;/p&gt;

&lt;p&gt;测试的执行过程为&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1). 测试脚本调用appium_lib提供的方法, 如 find, click
2). appium_lib调用转给Selenium WebDriver的Remote Adapter
3). Selenium WebDriver将请求封装为JSON，以HTTP Rest协议向Appium发送请求
4). Appium收到HTTP请求后，将请求交给相应的AppiumDriver
5). AppiumDriver通过HTTP向运行在终端设备上的WebDriverAgent进程(后台app)发送请求
6). 设备上的WebDriverAgent解析请求后，通过XCUITest测试套件与目标测试App交互，并获取结果
7). WebDriverAgent将结果由HTTPResponse返回给Appium
8). Appium再将结果进行包装处理后，由HTTPResponse返回给3)的Selenium WebDriver
9). Selenium WebDriver再将结果转换成Ruby对象返回给测试脚本。  
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id="2.3 Appium常用指令"&gt;2.3 Appium 常用指令&lt;/h3&gt;
&lt;p&gt;官方已经有了非常完整的文档，我也就不再重复或做些翻译工作了。
Ruby 语言请移步 &lt;a href="https://github.com/appium/ruby_lib/blob/master/docs/docs.md" rel="nofollow" target="_blank" title=""&gt;https://github.com/appium/ruby_lib/blob/master/docs/docs.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;p&gt;Example use of Appium's mobile gesture.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="/driver.find_element" class="user-mention" title="@driver.find_element"&gt;&lt;i&gt;@&lt;/i&gt;driver.find_element&lt;/a&gt;()&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;console.rb&lt;/code&gt; uses some code from &lt;a href="https://github.com/appium/appium/blob/82995f47408530c80c3376f4e07a1f649d96ba22/sample-code/examples/ruby/simple_test.rb" rel="nofollow" target="_blank" title=""&gt;simple_test.rb&lt;/a&gt; and is released under the &lt;a href="https://github.com/appium/appium/blob/c58eeb66f2d6fa3b9a89d188a2e657cca7cb300f/LICENSE" rel="nofollow" target="_blank" title=""&gt;same license&lt;/a&gt; as Appium. The &lt;a href="https://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/iPhoneAccessibility/Testing_Accessibility/Testing_Accessibility.html" rel="nofollow" target="_blank" title=""&gt;Accessibility Inspector&lt;/a&gt; is helpful for discovering button names and textfield values.&lt;/p&gt;

&lt;p&gt;Long click on an ImageView in Android.&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;last_image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_elements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:tag_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ImageView&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
&lt;span class="n"&gt;long_press&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;element: &lt;/span&gt;&lt;span class="n"&gt;last_image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rotate examples.&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt; &lt;span class="ss"&gt;:landscape&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rotate&lt;/span&gt; &lt;span class="ss"&gt;:portrait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;status["value"]["build"]["revision"]&lt;/code&gt; Discover the Appium rev running on the server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;driver.keyboard.send_keys "msg"&lt;/code&gt; Sends keys to currently active element&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="generic"&gt;generic&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; Prints a JSON view of the current page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page&lt;/code&gt; Prints the content descriptions and text values on the current page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page_class&lt;/code&gt; Prints the classes found on the current page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Element) find(value)&lt;/code&gt; Returns the first element that contains value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Element) finds(value)&lt;/code&gt; Returns all elements containing value (iOS only for now).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Element) name(name)&lt;/code&gt; Returns the first element containing name. Android name is the content description.
iOS uses accessibility label with a fallback to text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;Element&amp;gt;) names(name)&lt;/code&gt; Returns all elements containing name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Element) text(text)&lt;/code&gt; Returns the first element containing text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;Element&amp;gt;) texts(text)&lt;/code&gt; Returns all elements containing text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current_app&lt;/code&gt; Returns information about the current app. Android only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;--&lt;/p&gt;
&lt;h4 id="alert"&gt;alert&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;(void) alert_accept&lt;/code&gt; Accept the alert.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(String) alert_accept_text&lt;/code&gt; Get the text of the alert's accept button.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(void) alert_click(value)&lt;/code&gt; iOS only Tap the alert button identified by value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(void) alert_dismiss&lt;/code&gt; Dismiss the alert.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(String) alert_dismiss_text&lt;/code&gt; Get the text of the alert's dismiss button.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(String) alert_text&lt;/code&gt; Get the alert message text.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="button"&gt;button&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;(Button) button(index)&lt;/code&gt; Find a button by index.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Button) button(text)&lt;/code&gt; Get the first button that includes text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;String&amp;gt;, Array&amp;lt;Buttons&amp;gt;) buttons(text = nil)&lt;/code&gt; Get an array of button texts or button elements if text is provided.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;Button&amp;gt;) buttons(text)&lt;/code&gt; Get all buttons that include text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Button) first_button&lt;/code&gt; Get the first button element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Button) last_button&lt;/code&gt; Get the last button element.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="textfield"&gt;textfield&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;(Textfield) textfield(index)&lt;/code&gt; Find a textfield by index.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;Textfield&amp;gt;) textfields&lt;/code&gt; Get an array of textfield elements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Textfield) first_textfield&lt;/code&gt; Get the first textfield element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Textfield) last_textfield&lt;/code&gt; Get the last textfield element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Textfield) textfield_exact(text)&lt;/code&gt; Get the first textfield that matches text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Textfield) textfield(text)&lt;/code&gt; Get the first textfield that includes text.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="text"&gt;text&lt;/h4&gt;
&lt;p&gt;The Static Text methods have been prefixed with &lt;code&gt;s_&lt;/code&gt; to avoid conflicting with the generic text methods.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;(Text) text(index)&lt;/code&gt; Find a text by index.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Array&amp;lt;Text&amp;gt;) texts&lt;/code&gt; Get an array of text elements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Text) first_text&lt;/code&gt; Get the first text element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Text) last_text&lt;/code&gt; Get the last text element.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Text) text_exact(text)&lt;/code&gt; Get the first element that matches text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;(Text) text(text)&lt;/code&gt; Get the first textfield that includes text.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="window"&gt;window&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;(Object) window_size&lt;/code&gt; Get the window's size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;--&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="c1"&gt;# button, text&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="c1"&gt;# secure, textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s1"&gt;'some text'&lt;/span&gt; &lt;span class="c1"&gt;# type text into textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt; &lt;span class="c1"&gt;# clear textfield&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag_name&lt;/span&gt; &lt;span class="c1"&gt;# calls .type (patch.rb)&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;location&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rel_location&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt; &lt;span class="s1"&gt;'keys to send'&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_value&lt;/span&gt; &lt;span class="s1"&gt;'value to set'&lt;/span&gt; &lt;span class="c1"&gt;# ruby_console specific&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;displayed?&lt;/span&gt; &lt;span class="c1"&gt;# true or false depending if the element is visible&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selected?&lt;/span&gt; &lt;span class="c1"&gt;# is the tab selected?&lt;/span&gt;
&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'checked'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# is the checkbox checked?&lt;/span&gt;


&lt;span class="c1"&gt;# alert example without helper methods&lt;/span&gt;
&lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vg"&gt;$driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switch_to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;
&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;
&lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dismiss&lt;/span&gt;

&lt;span class="c1"&gt;# Secure textfield example.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Find using default value&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textfield&lt;/span&gt; &lt;span class="s1"&gt;'Password'&lt;/span&gt;
&lt;span class="c1"&gt;# Enter password&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_keys&lt;/span&gt; &lt;span class="s1"&gt;'hello'&lt;/span&gt;
&lt;span class="c1"&gt;# Check value&lt;/span&gt;
&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ios_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'hello'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;--&lt;/p&gt;
&lt;h4 id="Driver"&gt;Driver&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;start_driver&lt;/code&gt; will restart the driver.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;x&lt;/code&gt; will quit the driver and exit Pry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;execute_script&lt;/code&gt; calls &lt;code&gt;$driver.execute_script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;find_element&lt;/code&gt; calls &lt;code&gt;$driver.find_element&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;find_elements&lt;/code&gt; calls &lt;code&gt;$driver.find_elements&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;no_wait&lt;/code&gt; will set implicit wait to 0. &lt;code&gt;$driver.manage.timeouts.implicit_wait = 0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set_wait&lt;/code&gt; will set implicit wait to default seconds. &lt;code&gt;$driver.manage.timeouts.implicit_wait = default&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set_wait(timeout_seconds)&lt;/code&gt; will set implicit wait to desired timeout. &lt;code&gt;$driver.manage.timeouts.implicit_wait = timeout&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;.click to tap an element.
.send_keys to type on an element.&lt;/p&gt;
&lt;h4 id="Raw UIAutomation"&gt;Raw UIAutomation&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;execute_script "au.lookup('button')[0].tap()"&lt;/code&gt; is the same as
&lt;code&gt;execute_script 'UIATarget.localTarget().frontMostApp().buttons()[0].tap()'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://github.com/appium/appium/blob/master/app/uiauto/appium/app.js#L3" rel="nofollow" target="_blank" title=""&gt;app.js&lt;/a&gt; for more au methods.
Note that raw UIAutomation commands are not officially supported.&lt;/p&gt;

&lt;p&gt;Advanced au.&lt;/p&gt;

&lt;p&gt;In this example we lookup two tags, combine the results, wrap with $, and then return the elements.&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%(
var t = au.lookup('textfield');
var s = au.lookup('secure');
var r = $(t.concat(s));
au._returnElems(r);
)&lt;/span&gt;

&lt;span class="n"&gt;execute_script&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="XPath(UIAutomation)"&gt;XPath(UIAutomation)&lt;/h4&gt;
&lt;p&gt;See &lt;a href="https://github.com/appium/appium/pull/194/files" rel="nofollow" target="_blank" title=""&gt;#194&lt;/a&gt; for details.&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;find_element&lt;/span&gt;  &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button'&lt;/span&gt;
&lt;span class="n"&gt;find_elements&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button'&lt;/span&gt;

&lt;span class="n"&gt;find_element&lt;/span&gt;  &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button[@name="Sign In"]'&lt;/span&gt;
&lt;span class="n"&gt;find_elements&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button[@name="Sign In"]'&lt;/span&gt;

&lt;span class="n"&gt;find_element&lt;/span&gt;  &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button[contains(@name, "Sign In")]'&lt;/span&gt;
&lt;span class="n"&gt;find_elements&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'button[contains(@name, "Sign")]'&lt;/span&gt;

&lt;span class="n"&gt;find_element&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'textfield[@value="Email"]'&lt;/span&gt;
&lt;span class="n"&gt;find_element&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'textfield[contains(@value, "Email")]'&lt;/span&gt;

&lt;span class="n"&gt;find_element&lt;/span&gt;  &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text[contains(@name, "Reset")]'&lt;/span&gt;
&lt;span class="n"&gt;find_elements&lt;/span&gt; &lt;span class="ss"&gt;:xpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text[contains(@name, "agree")]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="3. Appium支持与无法支持的测试"&gt;3. Appium 支持与无法支持的测试&lt;/h2&gt;
&lt;p&gt;在 2.2 中，可以看到测试代码并不能直接访问被测试的 App 已及它的控件。因此能测试的也就是 XCUITest (iOS) 套件所支持的方法与属性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;支持的测试：&lt;/strong&gt;
查找元素，获取 name value type text size location enabled? displayed? selected？等属性，或者发送点击事件，为文本框输入文字。&lt;/p&gt;

&lt;p&gt;这些基本上都属于功能性测试，可以用来操作输入框，点击按钮，查找某段文字是否存在。&lt;/p&gt;

&lt;p&gt;如&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1) 打开登录页面
2) 找到 “用户名” 输入框，输入 test@test.com
3) 找到 “密码” 输入框， 输入 123456
4) 找到 “登录” 按钮，向它发生点击事件
5) 登录成功，检查用户是否处于主页面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;传送门
&lt;a href="https://github.com/appium/sample-code/blob/master/sample-code/examples/ruby/simple_test.rb" rel="nofollow" target="_blank" title=""&gt;https://github.com/appium/sample-code/blob/master/sample-code/examples/ruby/simple_test.rb&lt;/a&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GETTING STARTED&lt;/span&gt;
&lt;span class="c1"&gt;# -----------------&lt;/span&gt;
&lt;span class="c1"&gt;# This documentation is intended to show you how to get started with a&lt;/span&gt;
&lt;span class="c1"&gt;# simple Appium &amp;amp; appium_lib test.  This example is written without a specific&lt;/span&gt;
&lt;span class="c1"&gt;# testing framework in mind;  You can use appium_lib on any framework you like.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# INSTALLING RVM&lt;/span&gt;
&lt;span class="c1"&gt;# --------------&lt;/span&gt;
&lt;span class="c1"&gt;# If you don't have rvm installed, run the following terminal command&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# \curl -L https://get.rvm.io | bash -s stable --ruby&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# INSTALLING GEMS&lt;/span&gt;
&lt;span class="c1"&gt;# ---------------&lt;/span&gt;
&lt;span class="c1"&gt;# Then, change to the example directory:&lt;/span&gt;
&lt;span class="c1"&gt;#   cd appium-location/sample-code/examples/ruby&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# and install the required gems with bundler by doing:&lt;/span&gt;
&lt;span class="c1"&gt;#   bundle install&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# RUNNING THE TESTS&lt;/span&gt;
&lt;span class="c1"&gt;# -----------------&lt;/span&gt;
&lt;span class="c1"&gt;# To run the tests, make sure appium is running in another terminal&lt;/span&gt;
&lt;span class="c1"&gt;# window, then from the same window you used for the above commands, type&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# bundle exec ruby simple_test.rb&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# It will take a while, but once it's done you should get nothing but a line&lt;/span&gt;
&lt;span class="c1"&gt;# telling you "Tests Succeeded";  You'll see the iOS Simulator cranking away&lt;/span&gt;
&lt;span class="c1"&gt;# doing actions while we're running.&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rubygems'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;

&lt;span class="no"&gt;APP_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'../../apps/TestApp/build/release-iphonesimulator/TestApp.app'&lt;/span&gt;

&lt;span class="n"&gt;desired_caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;caps:       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;platformName:  &lt;/span&gt;&lt;span class="s1"&gt;'iOS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;versionNumber: &lt;/span&gt;&lt;span class="s1"&gt;'8.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;deviceName:    &lt;/span&gt;&lt;span class="s1"&gt;'iPhone 6'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;app:           &lt;/span&gt;&lt;span class="no"&gt;APP_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;appium_lib: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;sauce_username:   &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# don't run on Sauce&lt;/span&gt;
    &lt;span class="ss"&gt;sauce_access_key: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Start the driver&lt;/span&gt;
&lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desired_caps&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start_driver&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Calculator&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;IOS&lt;/span&gt;
    &lt;span class="c1"&gt;# Add all the Appium library methods to Test to make&lt;/span&gt;
    &lt;span class="c1"&gt;# calling them look nicer.&lt;/span&gt;
    &lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote_singleton_appium_methods&lt;/span&gt; &lt;span class="no"&gt;Calculator&lt;/span&gt;

    &lt;span class="c1"&gt;# Add two numbers&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;expected_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Find every textfield.&lt;/span&gt;
    &lt;span class="n"&gt;elements&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textfields&lt;/span&gt;

    &lt;span class="n"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_index&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# Click the first button&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

    &lt;span class="c1"&gt;# Get the first static text field, then get its text&lt;/span&gt;
    &lt;span class="n"&gt;actual_sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;actual_sum&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_sum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Alerts are visible&lt;/span&gt;
    &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'show alert'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="n"&gt;find_element&lt;/span&gt; &lt;span class="ss"&gt;:class_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'UIAAlert'&lt;/span&gt; &lt;span class="c1"&gt;# Elements can be found by :class_name&lt;/span&gt;

    &lt;span class="c1"&gt;# wait for alert to show&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="s1"&gt;'this alert is so cool'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Or by find&lt;/span&gt;
    &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Cancel'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

    &lt;span class="c1"&gt;# Waits until alert doesn't exist&lt;/span&gt;
    &lt;span class="n"&gt;wait_true&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UIAAlert'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Alerts can be switched into&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'show alert'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# Get a button by its text&lt;/span&gt;
    &lt;span class="n"&gt;alert&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switch_to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt; &lt;span class="c1"&gt;# Get the text of the current alert, using&lt;/span&gt;
    &lt;span class="c1"&gt;# the Selenium::WebDriver directly&lt;/span&gt;
    &lt;span class="n"&gt;alerting_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;alerting_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt; &lt;span class="s1"&gt;'Cool title'&lt;/span&gt;
    &lt;span class="n"&gt;alert_accept&lt;/span&gt; &lt;span class="c1"&gt;# Accept the current alert&lt;/span&gt;

    &lt;span class="c1"&gt;# Window Size is easy to get&lt;/span&gt;
    &lt;span class="n"&gt;sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_size&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;667&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;375&lt;/span&gt;

    &lt;span class="c1"&gt;# Quit when you're done!&lt;/span&gt;
    &lt;span class="n"&gt;driver_quit&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'Tests Succeeded!'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;提醒，由于 sample-code 中的 TestApp 比较老了，上面的测试代码直接跑，是会失败的。这个是很隐晦的问题导致，新版模拟器 (eg. iPhone 6 (iOS 10.0x)), 在运行这个 app 是，会自动弹出一个告警信息，说“TestApp 使用老版本编译的，会影响系统系能，建议用新 xcode 重新编译”。就是这个 alert 框，导致测试脚本找不到相应的控件而失败。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这个事情一开始也卡了我很久，为什么测试用例会通不过？后来是在用 Appium Ruby Console 的时候找到的。&lt;/p&gt;

&lt;p&gt;解决方式有两个：
一、重新编译 TestApp;&lt;/p&gt;

&lt;p&gt;二、测试代码在“ # Add two numbers”前加入 sleep(10) 暂停 10 秒，让你有机会点击 alert 上的 OK 按钮，关掉对话框。&lt;/p&gt;

&lt;p&gt;当时不想多事，选择了二。
现在想想，应该还有三：就是把 sleep 换成 alert_accept 来关掉对话框。有兴趣的可以自己试试。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;无法支持的测试：&lt;/strong&gt;
首先，测试脚本无法直接访问被测对象，其次，来之于上面 XCUITest 的限制，关于控件有无边框，控件状态，动画效果，字体变化，颜色，对齐等视觉效果，我们是无法测试的。&lt;/p&gt;

&lt;p&gt;关于这一点，stackoverflow 上也有老外在吐槽：
&lt;a href="http://stackoverflow.com/questions/31250941/xcuielement-obtain-image-value" rel="nofollow" target="_blank"&gt;http://stackoverflow.com/questions/31250941/xcuielement-obtain-image-value&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.danielhall.io/exploring-the-new-ui-testing-features-of-xcode-7" rel="nofollow" target="_blank"&gt;http://www.danielhall.io/exploring-the-new-ui-testing-features-of-xcode-7&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;如，&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a) &lt;em&gt;文章的标题需要根据不同的状态，用红色，黑色，灰色等不同的颜色展示。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;b) &lt;em&gt;当点击“上传”按钮后，头像 Image 应当加载新的头像地址。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这种测试通常就属于无法用自动化来检验的。&lt;/p&gt;

&lt;p&gt;尤其是我们的 App 是用 React Native 开发的，给我们的测试用例带来了更多的限制与挑战。&lt;/p&gt;
&lt;h2 id="4. 解决方案"&gt;4. 解决方案&lt;/h2&gt;
&lt;p&gt;自动化测试更多的是在功能性上，流程性上的测试起作用，基本能覆盖了 80%～90% 的功能性测试范围。&lt;/p&gt;

&lt;p&gt;对于自动化不好做的 AC，我们的处理方式很简单，把它们放到人工测试的范围去，当环境、需求、技术发生变化了，能自动化测试了，再自动化，不要太去纠结。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最后，自动化测试并不能代替一切，也无法覆盖所有的地方。&lt;/strong&gt;
它的最大价值在于使得 App 的开发也能够持续集成。
极大的降低了全回归的成本与工作量。&lt;/p&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Mon, 07 Aug 2017 09:04:17 +0800</pubDate>
      <link>https://ruby-china.org/topics/33747</link>
      <guid>https://ruby-china.org/topics/33747</guid>
    </item>
    <item>
      <title>敏捷实践 (1) - 我们是如何自动化 App 验收标准</title>
      <description>&lt;p&gt;简书：&lt;a href="http://www.jianshu.com/p/fd17c1bb6806" rel="nofollow" target="_blank"&gt;http://www.jianshu.com/p/fd17c1bb6806&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="1. 问题背景"&gt;1. 问题背景&lt;/h2&gt;
&lt;p&gt;Scrum 敏捷过程对交付质量有着严格的要求：&lt;strong&gt;零缺陷的&lt;a href="http://www.scrumcn.com/agile/scrum-knowledge-library/scrum.html#tab-id-9" rel="nofollow" target="_blank" title=""&gt;增量发布&lt;/a&gt;&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;要做到这一点意味着每个迭代发布都必须对 AC (Acceptance Criteria - 验收标准) 进行全回归，随着迭代中增量交付的增加，全回归的工作量与所需的时间也随之增加。&lt;/p&gt;

&lt;p&gt;我们在做 APP 开发过程中，&lt;strong&gt;app 开发人员&lt;/strong&gt; 告诉我这 app 自动化测试搞不了，&lt;strong&gt;测试人员&lt;/strong&gt; 也说，不好搞，很麻烦，而且还得靠录屏，blablabla.......  &lt;/p&gt;

&lt;p&gt;结果测试工作往往只能依赖人手进行测试，AC 要靠人手来全回归测试，就会发生这样一种情景：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;通常一个迭代的周期为两周 10 个工作日。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;迭代 1，计划完成 5 个功能，相应的 AC 也就不多，如果 1 天就能全部回归，那么开发有 9 天时间来实现功能。&lt;/li&gt;
&lt;li&gt;迭代 2，计划完成 4 个功能，相应的 AC 加上迭代 1 的 AC，需要 2 天才能全部回归，那么开发有 8 天的时间来实现功能。&lt;/li&gt;
&lt;li&gt;迭代 3，计划完成 3 个功能，相应的 AC 加上两个迭代的 AC，可能需要 3 天才能全部回归，那么开发有 7 天的时间来实现功能。&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每次迭代 AC 全回归的所需的时间在不断增加，而能用于开发的时间不断在受到挤压而减少，能交付的功能也就不断在减少。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要质量就没速度，如此还能敏捷的起来吗？&lt;/li&gt;
&lt;li&gt;不少团队 (包括我们) 采用了牺牲交付质量 (仅部分回归甚至无回归) 来确保开发进度，这样做既不符合敏捷过程的价值观，实际上也没有达到交付目标。&lt;/li&gt;
&lt;li&gt;还有一个土豪的做法，就是增配测试。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们一直在探索这个问题的解决方案，既想确保增量发布的速度，又想拥有交付质量。只能想办法把 AC 自动化起来，全回归就不需要占用太多人手，而且还能随时进行，所需要的时间也大大缩短。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 解决方案"&gt;2. 解决方案&lt;/h2&gt;
&lt;p&gt;在 2017 年 1 月份的 ScrumMaster 培训课中，聊起 App 自动化测试有什么工具，便有小伙伴提了一下 Appium。回来后便查资料研究，发现有两个非常不错的自动化工具，一个就是 Appium，另一个是阿里系的 Macaca。这里不得不说，Macaca 做的很不错，javascript 写测试也相对容易上手。&lt;/p&gt;

&lt;p&gt;然而，做了对比后，我决定还是选用 Appium。原因很简单，Appium 支持 Ruby 和 Cucumber，我们的后端正好是用 RubyOnRails 写的，后端开发人员已经在写后端过程中积累了许多编写自动化测试用例的经验。正好也可以来写 app 的自动化测试。&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="2.1 环境准备"&gt;2.1 环境准备&lt;/h3&gt;
&lt;p&gt;以下内容的运行环境为 MacOS 10.12 + Ruby 2.3.3 + NodeJS 7.x。 
至于如何安装 Ruby 和 NodeJS，这个应当为基本技能，请自行搞定。&lt;/p&gt;

&lt;p&gt;请不要问我如何在 Windows 上去搭建环境的有关问题，我只会建议要么白苹果，要么黑苹果，要么 Ubuntu (不支持 iOS)。要么走您。&lt;/p&gt;

&lt;p&gt;由于 npmjs.org  rubygems.org 都在国外，访问速度慢且不稳定，建议先搭好梯子，或者改用国内的源。&lt;/p&gt;

&lt;hr&gt;
&lt;h4 id="2.1.1 Appium 1.6.3 安装"&gt;2.1.1 Appium 1.6.3 安装&lt;/h4&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nt"&gt;-g&lt;/span&gt; appium
npm &lt;span class="nt"&gt;-g&lt;/span&gt; appium-doctor
&lt;span class="c"&gt;# ios还需要安装两个工具 ios-deploy 用于安装app到真机；Carthage 用于安装app到模拟器&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;carthage
brew &lt;span class="nb"&gt;install &lt;/span&gt;ios-deploy
&lt;span class="c"&gt;# brew 是个什么鬼，如何安装的？没什么技术含量，请自行baidu&lt;/span&gt;

&lt;span class="c"&gt;# 完成后可以运行appium-doctor诊断一下环境是否正常&lt;/span&gt;
&lt;span class="c"&gt;# 如果没有android需求的，可以不用管 JAVA_HOME 与 android adb 等错误&lt;/span&gt;
appium-doctor

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切正常会得到类似这个信息&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-c3b684a3ce7017b8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="appium-doctor-screenshot.png"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h4 id="2.1.2 安装 Appium 的 Ruby 驱动，和appium ruby console"&gt;2.1.2 安装 Appium 的 Ruby 驱动，和 appium ruby console&lt;/h4&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;appium_lib&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;appium_console&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，环境准备工作已经完成。 &lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="2.2 编写自动化测试用例"&gt;2.2 编写自动化测试用例&lt;/h3&gt;
&lt;p&gt;测试用例采用 Cucumber 方式编写，Feature 对应 用户故事，Scenario 对应 AC。
一个 Feature 包含多个 Scenarios。
一个用户故事有多个 AC 验收标准。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;至于 Cucumber, 有空我会再写一个文章。目前请先自行 baidu 之。&lt;/em&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h4 id="2.2.1 创建目录"&gt;2.2.1 创建目录&lt;/h4&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;app_test
&lt;span class="nb"&gt;cd &lt;/span&gt;app_test
&lt;span class="c"&gt;# 创建测试用例目录&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;features 
&lt;span class="c"&gt;# 创建测试用例步骤目录&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;features/steps
&lt;span class="c"&gt;# 创建支持目录&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;features/support
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id="2.2.2 准备Ruby Cucumber的Gemfile"&gt;2.2.2 准备 Ruby Cucumber 的 Gemfile&lt;/h4&gt;
&lt;p&gt;$ cat Gemfile&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://gems.ruby-china.org'&lt;/span&gt;

&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="s1"&gt;'~&amp;gt; 9.3.0'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rest-client'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'~&amp;gt; 1.6.7'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rspec'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="s1"&gt;'~&amp;gt; 2.14.1'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'cucumber'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="s1"&gt;'~&amp;gt; 1.3.15'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rspec-expectations'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 2.14.5'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'spec'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="s1"&gt;'~&amp;gt; 5.3.4'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'sauce_whisk'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="s1"&gt;'~&amp;gt; 0.0.13'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'test-unit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'~&amp;gt; 2.5.5'&lt;/span&gt; &lt;span class="c1"&gt;# required for bundle exec ruby xunit_android.rb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 gem 包&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle install
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id="2.2.3 cucumber env"&gt;2.2.3 cucumber env&lt;/h4&gt;
&lt;p&gt;$ cat features/support/env.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This file provides setup and common functionality across all features.  It's&lt;/span&gt;
&lt;span class="c1"&gt;# included first before every test run, and the methods provided here can be &lt;/span&gt;
&lt;span class="c1"&gt;# used in any of the step definitions used in a test.  This is a great place to&lt;/span&gt;
&lt;span class="c1"&gt;# put shared data like the location of your app, the capabilities you want to&lt;/span&gt;
&lt;span class="c1"&gt;# test with, and the setup of selenium.&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'rspec/expectations'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'appium_lib'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'cucumber/ast'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'sauce_whisk'&lt;/span&gt;

&lt;span class="c1"&gt;# Create a custom World class so we don't pollute `Object` with Appium methods&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppiumWorld&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Load the desired configuration from appium.txt, create a driver then&lt;/span&gt;
&lt;span class="c1"&gt;# Add the methods to the world&lt;/span&gt;
&lt;span class="n"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_appium_txt&lt;/span&gt; &lt;span class="ss"&gt;file: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;verbose: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

&lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;Appium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;promote_appium_methods&lt;/span&gt; &lt;span class="no"&gt;AppiumWorld&lt;/span&gt;

&lt;span class="no"&gt;World&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;AppiumWorld&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# 结束时，推出驱动&lt;/span&gt;
&lt;span class="nb"&gt;at_exit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver_quit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# 在运行标有@reset_driver的Scenario前重启驱动&lt;/span&gt;
&lt;span class="no"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"@reset_driver"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="vg"&gt;$driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restart&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id="2.2.4 appium配置文件 appium.txt"&gt;2.2.4 appium 配置文件 appium.txt&lt;/h4&gt;
&lt;p&gt;关于 appium.txt 配置说明可以参考文档 &lt;a href="http://appium.io/slate/en/master/?ruby#appium-server-capabilities" rel="nofollow" target="_blank" title=""&gt;appium-server-capabilities&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;$ cat features/support/appium.txt&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;platformName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ios"&lt;/span&gt;
&lt;span class="n"&gt;deviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"iPhone 6s"&lt;/span&gt;
&lt;span class="n"&gt;platformVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.2"&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'../ios/build/Build/Products/Debug-iphonesimulator/PuKe.app'&lt;/span&gt;
&lt;span class="n"&gt;automationName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'XCUITest'&lt;/span&gt;
&lt;span class="n"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'zh'&lt;/span&gt;
&lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'zh_CN'&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;appium_lib&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;sauce_username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;sauce_access_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id="2.3 编写测试用例"&gt;2.3 编写测试用例&lt;/h3&gt;
&lt;p&gt;每一用户故事的测试用例编写为一个 feature。&lt;/p&gt;

&lt;hr&gt;
&lt;h4 id="2.3.1 编写邮箱登陆用户故事的AC"&gt;2.3.1 编写邮箱登陆用户故事的 AC&lt;/h4&gt;
&lt;p&gt;邮箱登陆用户故事只是登陆故事中的其中一个，故事 ID US004&lt;/p&gt;

&lt;p&gt;$cat features/US004_login_by_email.feature&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;US_004&lt;/span&gt; &lt;span class="n"&gt;邮箱登录&lt;/span&gt;
  &lt;span class="n"&gt;为了正常使用需要登录身份的功能&lt;/span&gt;
  &lt;span class="n"&gt;作为一个已经用邮箱注册过的用户&lt;/span&gt;
  &lt;span class="n"&gt;我想要用邮箱和密码登录系统&lt;/span&gt;

  &lt;span class="vi"&gt;@reset_driver&lt;/span&gt;
  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_02&lt;/span&gt; &lt;span class="n"&gt;登录错误&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;正确邮箱&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;错误密码登录&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt; &lt;span class="n"&gt;点击&lt;/span&gt; &lt;span class="s2"&gt;"登录/注册"&lt;/span&gt; &lt;span class="n"&gt;进入&lt;/span&gt; &lt;span class="s2"&gt;"登录页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test_user@mytest.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"b123456"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"用户密码不匹配"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_03&lt;/span&gt; &lt;span class="n"&gt;登录错误&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;没有输入用户名和密码&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"请填写完整"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_04&lt;/span&gt; &lt;span class="n"&gt;登录错误&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;输入用户名没有输入密码&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test_user@mytest.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当看到浮动提示&lt;/span&gt; &lt;span class="s2"&gt;"请填写完整"&lt;/span&gt;

  &lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_01&lt;/span&gt; &lt;span class="n"&gt;正常邮箱&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;密码登录&lt;/span&gt;
    &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
    &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test_user@mytest.com"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test123"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
    &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt;
    &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;等待&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="n"&gt;秒后退出&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id="2.3.2 为feature编写相应的steps"&gt;2.3.2 为 feature 编写相应的 steps&lt;/h4&gt;
&lt;p&gt;Scenario 中的每个 Given When Then And 都在 steps.rb 中有对应的定义。&lt;/p&gt;

&lt;p&gt;$ cat features/step_definitions/steps.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# These are the 'step definitions' which Cucumber uses to implement features.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Each step starts with a regular expression matching the step you write in&lt;/span&gt;
&lt;span class="c1"&gt;# your feature description.  Any variables are parsed out and passed to the&lt;/span&gt;
&lt;span class="c1"&gt;# step block.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# The instructions in the step are then executed with those variables.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# In this example, we're using rspec's assertions to test that things are happening,&lt;/span&gt;
&lt;span class="c1"&gt;# but you can use any ruby code you want in the steps.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# The '$driver' object is the appium_lib driver, set up in the cucumber/support/env.rb&lt;/span&gt;
&lt;span class="c1"&gt;# file, which is a convenient place to put it as we're likely to use it often.&lt;/span&gt;
&lt;span class="c1"&gt;# This is a different use to most of the examples;  Cucumber steps are instances&lt;/span&gt;
&lt;span class="c1"&gt;# of `Object`, and extending Object with Appium methods (through &lt;/span&gt;
&lt;span class="c1"&gt;# `promote_appium_methods`) is a bad idea.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# For more on step definitions, check out the documentation at&lt;/span&gt;
&lt;span class="c1"&gt;# https://github.com/cucumber/cucumber/wiki/Step-Definitions&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# For more on rspec assertions, check out&lt;/span&gt;
&lt;span class="c1"&gt;# https://www.relishapp.com/rspec/rspec-expectations/docs&lt;/span&gt;

&lt;span class="no"&gt;Given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我已经用邮箱 (.*) 与密码 (.*) 注册过账号$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# sleep(1)&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: email: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: password: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "主页面" 点击 "登录\/注册" 进入 "登录页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# 等待主页面就绪, 主页面ID 为 home_page&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;# 点击 主页面中的 '登录/注册' 按钮，按钮ID为 btn_to_login&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_to_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;

  &lt;span class="c1"&gt;# 检查页面跳转到 登录页面， 登录页面ID为 page_login_account&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page_login_account'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;When&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我在 "(.*?)" 输入 "(.*?)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;input_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;input_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;input_field&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'邮箱或手机'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_username'&lt;/span&gt;
               &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s1"&gt;'密码'&lt;/span&gt;
                 &lt;span class="s1"&gt;'input_password'&lt;/span&gt;
               &lt;span class="k"&gt;else&lt;/span&gt;
                 &lt;span class="s1"&gt;'unknown'&lt;/span&gt;
             &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# 定位指定的输入框&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;                    &lt;span class="c1"&gt;# 清除原来的内容&lt;/span&gt;
  &lt;span class="n"&gt;input_box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;input_value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# 输入新内容并回车&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我按下按钮 "登录"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'btn_login'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当看到浮动提示 "(.+)"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip!&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"DEBUG: 期待 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;Then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^我应当到达 "主页面"$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'home_page'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="no"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^等待 (\d+) 秒后.*/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="3. 运行测试用例"&gt;3. 运行测试用例&lt;/h2&gt;&lt;h3 id="3.1 运行 Appium"&gt;3.1 运行 Appium&lt;/h3&gt;
&lt;p&gt;运行测试用例前必须先启动 appium,
新打开一个命令行窗口
&lt;code&gt;$ appium&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-610134d35c5dcd23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="run_appium.png"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="3.2 运行测试用例"&gt;3.2 运行测试用例&lt;/h3&gt;
&lt;p&gt;在 test_app 目录中&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$ cucumber features/US004_login_by_email.feature&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="http://upload-images.jianshu.io/upload_images/4753959-38e345cf0d1301ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" title="" alt="run_cucumber_feature.png"&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="3.3 观看测试用例的运行录屏"&gt;3.3 观看测试用例的运行录屏&lt;/h3&gt;
&lt;p&gt;&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//player.youku.com/embed/XMjUyMTkwODUzMg==" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="3.4 示例代码参考"&gt;3.4 示例代码参考&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/appium/sample-code/" rel="nofollow" target="_blank"&gt;https://github.com/appium/sample-code/&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 总结"&gt;4. 总结&lt;/h2&gt;
&lt;p&gt;抛开复杂的 appium、cucumber steps，发现没，测试用例是不是很容易理解？&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;AC_US004_01&lt;/span&gt; &lt;span class="n"&gt;正常邮箱&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;密码登录&lt;/span&gt;
  &lt;span class="no"&gt;Given&lt;/span&gt; &lt;span class="n"&gt;我已经用邮箱&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="vi"&gt;@mytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;com&lt;/span&gt; &lt;span class="n"&gt;与密码&lt;/span&gt; &lt;span class="n"&gt;test123&lt;/span&gt; &lt;span class="n"&gt;注册过账号&lt;/span&gt;
  &lt;span class="no"&gt;When&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"邮箱或手机"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test_user@mytest.com"&lt;/span&gt;
  &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我在&lt;/span&gt; &lt;span class="s2"&gt;"密码"&lt;/span&gt; &lt;span class="n"&gt;输入&lt;/span&gt; &lt;span class="s2"&gt;"test123"&lt;/span&gt;
  &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;我按下按钮&lt;/span&gt; &lt;span class="s2"&gt;"登录"&lt;/span&gt;
  &lt;span class="no"&gt;Then&lt;/span&gt; &lt;span class="n"&gt;我应当到达&lt;/span&gt; &lt;span class="s2"&gt;"主页面"&lt;/span&gt;
  &lt;span class="no"&gt;And&lt;/span&gt; &lt;span class="n"&gt;等待&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="n"&gt;秒后退出&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像上面的 AC，就算是非技术人员也能分分钟写出来，我们的产品经理、UI 设计师都在参与编写 AC，这就是我们选择 cucumber ruby 的根本原因。 &lt;/p&gt;

&lt;p&gt;本来打算一篇写完的，写着写着，发现要写的内容太多了，决定还是写成一个系列：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首先、用 Appium 来测试，也存在一些坑要填。&lt;/li&gt;
&lt;li&gt;上面的 feature 和 steps，为了演示好理解，做了相当的简化。实际上，我们的 steps 经过一些重构，变得很通用灵活，后续放出来。&lt;/li&gt;
&lt;li&gt;Appium 的配置与基本指令&lt;/li&gt;
&lt;li&gt;辅助神器 Appium Ruby Console (ARC) 的使用&lt;/li&gt;
&lt;li&gt;如何用 Cucumber 编写自动化测试&lt;/li&gt;
&lt;li&gt;React-Native 类型的 APP 自动化有哪些坑需要绕过&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有空的时候再接着写（二）、（三）。。。。。。&lt;/p&gt;</description>
      <author>edwardzhou</author>
      <pubDate>Mon, 07 Aug 2017 09:00:05 +0800</pubDate>
      <link>https://ruby-china.org/topics/33746</link>
      <guid>https://ruby-china.org/topics/33746</guid>
    </item>
  </channel>
</rss>
