tabbyx 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/tabbyx.rb +29 -0
- data/lib/tabbyx/calabash_android.rb +11 -0
- data/lib/tabbyx/calabash_ios.rb +4 -0
- data/lib/tabbyx/core/base.rb +54 -0
- data/lib/tabbyx/core/config.rb +36 -0
- data/lib/tabbyx/core/initialize.rb +35 -0
- data/lib/tabbyx/fixtures/constants.rb +21 -0
- data/lib/tabbyx/fixtures/global.rb +39 -0
- data/lib/tabbyx/helpers/csv_helper.rb +65 -0
- data/lib/tabbyx/helpers/debug_helper.rb +37 -0
- data/lib/tabbyx/helpers/excel_helper.rb +79 -0
- data/lib/tabbyx/helpers/http_batch_handler.rb +50 -0
- data/lib/tabbyx/helpers/http_helper.rb +46 -0
- data/lib/tabbyx/helpers/minitest_helper.rb +48 -0
- data/lib/tabbyx/helpers/screenshot_helpers.rb +128 -0
- data/lib/tabbyx/helpers/txt_helper.rb +35 -0
- data/lib/tabbyx/steps_android/assert_steps.rb +31 -0
- data/lib/tabbyx/steps_android/check_box_steps.rb +3 -0
- data/lib/tabbyx/steps_android/context_menu_steps.rb +17 -0
- data/lib/tabbyx/steps_android/date_picker_steps.rb +8 -0
- data/lib/tabbyx/steps_android/enter_text_steps.rb +23 -0
- data/lib/tabbyx/steps_android/location_steps.rb +7 -0
- data/lib/tabbyx/steps_android/navigation_steps.rb +47 -0
- data/lib/tabbyx/steps_android/press_button_steps.rb +39 -0
- data/lib/tabbyx/steps_android/progress_steps.rb +51 -0
- data/lib/tabbyx/steps_android/search_steps.rb +7 -0
- data/lib/tabbyx/steps_android/spinner_steps.rb +11 -0
- data/lib/tabbyx/steps_ios/assertions.rb +79 -0
- data/lib/tabbyx/steps_ios/date_picker.rb +39 -0
- data/lib/tabbyx/steps_ios/operations.rb +187 -0
- data/lib/tabbyx/steps_ios/wait.rb +48 -0
- data/lib/tabbyx/utils/adb_devices.rb +248 -0
- data/lib/tabbyx/utils/adb_screenshot.rb +22 -0
- data/lib/tabbyx/utils/code_coverage.rb +6 -0
- data/lib/tabbyx/utils/shell.rb +32 -0
- data/lib/tabbyx/version.rb +3 -0
- metadata +286 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'excel_helper'
|
3
|
+
require 'http_helper'
|
4
|
+
|
5
|
+
=begin
|
6
|
+
使用方法:
|
7
|
+
1. 将API的url,参数,action等按照模板 inputs/api_testcases.xlsx 填写
|
8
|
+
TESTCASE ACTION URL PARAMS
|
9
|
+
2. 运行测试,接口请求返回的结果会填回表格
|
10
|
+
example:
|
11
|
+
RunTestCases.new("api_testcases.xlsx",0).run_test_cases
|
12
|
+
3. 运行完毕后打开excel文件查看返回数据
|
13
|
+
=end
|
14
|
+
class BatchGetResponse
|
15
|
+
attr_accessor :testcases
|
16
|
+
|
17
|
+
def initialize(filename,worksheet=0)
|
18
|
+
@testcases = ExcelHelper.read_from_excel(filename,worksheet)
|
19
|
+
@filename = filename
|
20
|
+
@worksheet = worksheet
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_test_cases
|
24
|
+
testresults = []
|
25
|
+
@testcases.each do |testcase|
|
26
|
+
testresult = []
|
27
|
+
unless testcase[1].nil?
|
28
|
+
(0..3).each { |i| testresult[i] = testcase[i] }
|
29
|
+
action = testcase[1].strip
|
30
|
+
url = testcase[2].strip
|
31
|
+
params = testcase[3]
|
32
|
+
end
|
33
|
+
|
34
|
+
case action
|
35
|
+
when 'GET'
|
36
|
+
res = HTTPHelper.new.get_response(HOST+url,params)
|
37
|
+
testresult << res << res.code << res["Data"]
|
38
|
+
when 'POST'
|
39
|
+
res = HTTPHelper.new.post_response(HOST+url,params)
|
40
|
+
testresult << res << res.code << res["Data"]
|
41
|
+
when 'ACTION'
|
42
|
+
testresult << "RESPONSE" << "RESPONSE CODE" << "DATA"
|
43
|
+
end
|
44
|
+
|
45
|
+
testresults.push testresult
|
46
|
+
end
|
47
|
+
ExcelHelper.write_dictionary_to_excel(testresults,"testresult1.xlsx",@worksheet) unless testresults.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'tabbyx/fixtures/global'
|
3
|
+
|
4
|
+
module HTTPHelper
|
5
|
+
@header =
|
6
|
+
{
|
7
|
+
'Content-Type' => 'application/json',
|
8
|
+
'Accept' => 'application/json',
|
9
|
+
'User-Agent' => USER_AGENT,
|
10
|
+
'Cookie' => COOKIE
|
11
|
+
}.to_hash
|
12
|
+
|
13
|
+
def self.post_response(requestUrl,params=nil,serviceRequest=nil,query=nil)
|
14
|
+
@body =
|
15
|
+
{
|
16
|
+
:serviceRequest => serviceRequest
|
17
|
+
}
|
18
|
+
|
19
|
+
params ? request = requestUrl+'?'+params : request = requestUrl
|
20
|
+
puts "请求: " + request
|
21
|
+
|
22
|
+
if serviceRequest.nil?
|
23
|
+
response = HTTParty.post(request, :query => query, :headers => @header)
|
24
|
+
else
|
25
|
+
response = HTTParty.post(request, :body => @body, :headers => @header)
|
26
|
+
end
|
27
|
+
|
28
|
+
response
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.get_response(requestUrl, params=nil, query=nil)
|
32
|
+
params ? request = requestUrl+'?'+params : request = requestUrl
|
33
|
+
query ? q = query.to_hash : q = query
|
34
|
+
response = HTTParty.get(request, :query => q, :headers => @header)
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.response_code(response)
|
39
|
+
response.code
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.response_data(response)
|
43
|
+
response["Data"] || response["data"]
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# require this file if you want to use minitest-reporter
|
2
|
+
|
3
|
+
require 'minitest/reporters'
|
4
|
+
|
5
|
+
# test report for rake testing(API & unit testing)
|
6
|
+
module Minitest
|
7
|
+
module Reporters
|
8
|
+
class AwesomeReporter < HtmlReporter
|
9
|
+
GRAY = '0;36'
|
10
|
+
GREEN = '1;32'
|
11
|
+
RED = '1;31'
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
super
|
15
|
+
@slow_threshold = options.fetch(:slow_threshold, nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def record_pass(test)
|
19
|
+
if @slow_threshold.nil? || test.time <= @slow_threshold
|
20
|
+
super
|
21
|
+
else
|
22
|
+
gray('O')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def color_up(string, color)
|
27
|
+
color? ? "\e\[#{ color }m#{ string }#{ ANSI::Code::ENDCODE }" : string
|
28
|
+
end
|
29
|
+
|
30
|
+
def red(string)
|
31
|
+
color_up(string, RED)
|
32
|
+
end
|
33
|
+
|
34
|
+
def green(string)
|
35
|
+
color_up(string, GREEN)
|
36
|
+
end
|
37
|
+
|
38
|
+
def gray(string)
|
39
|
+
color_up(string, GRAY)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
reporter_options = { color: true, slow_count: 5, reports_dir:'reports'}
|
47
|
+
report = Minitest::Reporters::AwesomeReporter.new(reporter_options)
|
48
|
+
Minitest::Reporters.use! [report]
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# This requires ImageMagick to be installed
|
2
|
+
|
3
|
+
def screenshot_dirs
|
4
|
+
base_dir = screenshot_base_dir
|
5
|
+
orig_dir = base_dir + 'orig'
|
6
|
+
check_dir = base_dir + 'check'
|
7
|
+
diff_dir = base_dir + 'diff'
|
8
|
+
|
9
|
+
# Create directories if they don't exist
|
10
|
+
FileUtils.mkdir_p(orig_dir)
|
11
|
+
FileUtils.mkdir_p(check_dir)
|
12
|
+
FileUtils.mkdir_p(diff_dir)
|
13
|
+
|
14
|
+
{:orig_dir => orig_dir, :check_dir => check_dir, :diff_dir => diff_dir}
|
15
|
+
end
|
16
|
+
|
17
|
+
def remember_screen_as(screen)
|
18
|
+
screen.tr!(" ", "_")
|
19
|
+
dirs = screenshot_dirs
|
20
|
+
orig_filename = "#{dirs[:orig_dir]}/" + "#{screen}.png"
|
21
|
+
capture_filename = screenshot({:prefix => "#{dirs[:orig_dir]}/", :name => "#{screen}.png"})
|
22
|
+
puts orig_filename
|
23
|
+
puts capture_filename
|
24
|
+
if (orig_filename != capture_filename)
|
25
|
+
FileUtils.mv(capture_filename,orig_filename) # calabash might add a counter to the file
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def compare_screen(screen)
|
30
|
+
screen.tr!(" ", "_")
|
31
|
+
dirs = screenshot_dirs
|
32
|
+
|
33
|
+
screenshot_filename = screenshot({:prefix => "#{dirs[:check_dir]}/", :name => "#{screen}.png"})
|
34
|
+
check_filename = "#{dirs[:check_dir]}/#{screen}.png"
|
35
|
+
check_rotated_filename = "#{dirs[:check_dir]}/#{screen}_rotated.png"
|
36
|
+
orig_filename = "#{dirs[:orig_dir]}/#{screen}.png"
|
37
|
+
diff_filename = "#{dirs[:diff_dir]}/#{screen}.png"
|
38
|
+
diff_rotated_filename = "#{dirs[:diff_dir]}/#{screen}_rotated.png"
|
39
|
+
|
40
|
+
# Moving files is instant, sleep 1 sec is needed to ensure that the movement is done before compaing two images.
|
41
|
+
FileUtils.mv(screenshot_filename,check_filename) # discard number, only keep one
|
42
|
+
sleep 1
|
43
|
+
|
44
|
+
if (File.exists? orig_filename)
|
45
|
+
# If the two images don't have the same width and height, ImageMagic's compare method will throw an exception
|
46
|
+
# get the original image's dimension
|
47
|
+
orig_image_size = (%x[identify -format "%[fx:w]x%[fx:h]" "#{orig_filename}"]).chomp # might be 1536 × 2048, 768x1024 or return error
|
48
|
+
|
49
|
+
# get the dimension of the image that is to be checked
|
50
|
+
check_image_size = (%x[identify -format "%[fx:w]x%[fx:h]" "#{check_filename}"]).chomp
|
51
|
+
|
52
|
+
# occasionally identify method isn't reliable, you will get 0x0 above, the image's width and height should have 3-4 digits
|
53
|
+
if /^(\d{3,4})x(\d{3,4})$/.match(orig_image_size).nil?
|
54
|
+
fail(msg="Cannot get image size, please replace the original image")
|
55
|
+
end
|
56
|
+
|
57
|
+
if /^(\d{3,4})x(\d{3,4})$/.match(check_image_size).nil?
|
58
|
+
fail(msg="the original screenshot's info isn't readable to ImageMagic, please check.")
|
59
|
+
end
|
60
|
+
|
61
|
+
# convert the check_image to the same size of the orig_image if necessary, so they are comparable
|
62
|
+
%x[convert "#{check_filename}" -resize #{orig_image_size} "#{check_filename}"] unless orig_image_size == check_image_size
|
63
|
+
sleep 2
|
64
|
+
# Create a rotated version of the screenshot
|
65
|
+
# shell_res = %x[convert "#{check_filename}" -rotate 180 #{check_rotated_filename}]
|
66
|
+
|
67
|
+
# Compare pixel-by-pixel
|
68
|
+
# -fuzz %1 is too strict, change it to 5%
|
69
|
+
diff = (%x[compare -metric AE -fuzz 5% "#{orig_filename}" "#{check_filename}" "#{diff_filename}" 2>&1]).chomp
|
70
|
+
diff_rotated = (%x[compare -metric AE -fuzz 5% "#{orig_filename}" "#{check_rotated_filename}" "#{diff_rotated_filename}" 2>&1]).chomp
|
71
|
+
if (!diff.match(/^\d.*$/))
|
72
|
+
fail(msg="Error comparing pictures. '#{diff}'")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Use rotated file if it has a better match
|
76
|
+
if (diff.to_f > diff_rotated.to_f)
|
77
|
+
log "Using rotated screenshot instead, since it provides a better match"
|
78
|
+
FileUtils.cp(check_rotated_filename,check_filename)
|
79
|
+
FileUtils.cp(diff_rotated_filename,diff_filename)
|
80
|
+
diff = diff_rotated
|
81
|
+
end
|
82
|
+
FileUtils.rm check_rotated_filename
|
83
|
+
FileUtils.rm diff_rotated_filename
|
84
|
+
|
85
|
+
pixel_count = (%x[convert "#{orig_filename}" -format "%[fx:w*h]" info:]).chomp # occasionally unreliable
|
86
|
+
percent_diff = 100*diff.to_f/pixel_count.to_f
|
87
|
+
puts "Screenshots diff: #{percent_diff}"
|
88
|
+
|
89
|
+
# Remove diff if no difference
|
90
|
+
FileUtils.rm diff_filename if (percent_diff == 0)
|
91
|
+
|
92
|
+
else
|
93
|
+
# No original file. Use the new screenshot. Keep the one in "check".
|
94
|
+
FileUtils.cp(check_filename,orig_filename)
|
95
|
+
log "No original screenshot. New has been created at #{orig_filename}. Please check."
|
96
|
+
percent_diff = 0
|
97
|
+
end
|
98
|
+
|
99
|
+
{
|
100
|
+
:check_filename => check_filename,
|
101
|
+
:orig_filename => orig_filename,
|
102
|
+
:diff_filename => diff_filename,
|
103
|
+
:percent_diff => percent_diff
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def screen_match?(screen, x = 100, invert = false, always_clean = false)
|
108
|
+
res = compare_screen(screen)
|
109
|
+
|
110
|
+
FileUtils.rm res[:check_filename] if always_clean
|
111
|
+
|
112
|
+
return false if (!invert && (100-res[:percent_diff]) < x.to_f)
|
113
|
+
return false if (invert && (100-res[:percent_diff]) >= x.to_f)
|
114
|
+
|
115
|
+
# Remove check file if everything is ok
|
116
|
+
FileUtils.rm res[:check_filename] if !always_clean
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def screen_not_match?(screen, x = 100) ; screen_match?(screen, x, true) end
|
122
|
+
|
123
|
+
def assert_screen_match(screen,x = 100,invert = false)
|
124
|
+
match = screen_match?(screen,x,invert)
|
125
|
+
fail(msg="Screenshots differ too much.") if !match && !invert
|
126
|
+
fail(msg="Screenshots differ too little.") if !match && invert
|
127
|
+
true
|
128
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'tabbyx/core/base'
|
2
|
+
|
3
|
+
module TXThelper
|
4
|
+
|
5
|
+
# example:
|
6
|
+
# TXThelper.read_from_text('api_testcases.csv')
|
7
|
+
|
8
|
+
def self.read_from_text(filename)
|
9
|
+
file = Base.file_exists?(filename)
|
10
|
+
text = []
|
11
|
+
lines = IO.readlines(file)
|
12
|
+
lines.each do |line|
|
13
|
+
text.push line
|
14
|
+
end
|
15
|
+
text
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.read_file_by_path(file_path)
|
19
|
+
file = File.absolute_path(file_path)
|
20
|
+
data = []
|
21
|
+
lines = IO.readlines(file)
|
22
|
+
lines.each do |line|
|
23
|
+
data.push line
|
24
|
+
end
|
25
|
+
data
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.write_array_to_text(array,filename)
|
29
|
+
Base.file_exists?(filename) ? file = Base.file_exists?(filename) : file = Base.create_file(filename)
|
30
|
+
File.open(file,"w") do |line|
|
31
|
+
array.each_with_index { |item,index | line.print(index,":",item);line.puts "\n" }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
Then /^我看见文本"([^\"]*)"$/ do |text|
|
3
|
+
wait_for_text(text, timeout: 10)
|
4
|
+
end
|
5
|
+
|
6
|
+
Then /^我看见"([^\"]*)"$/ do |text|
|
7
|
+
wait_for_text(text, timeout: 10)
|
8
|
+
end
|
9
|
+
|
10
|
+
Then /^我必须看见"([^\"]*)"$/ do |text|
|
11
|
+
wait_for_text(text, timeout: 10)
|
12
|
+
end
|
13
|
+
|
14
|
+
Then /^我必须看见文本包含"([^\"]*)"$/ do |text|
|
15
|
+
wait_for_text(text, timeout: 10)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Then /^我看不到文本"([^\"]*)"$/ do |text|
|
21
|
+
wait_for_text_to_disappear(text, timeout: 10)
|
22
|
+
end
|
23
|
+
|
24
|
+
Then /^我看不见文本"([^\"]*)"$/ do |text|
|
25
|
+
wait_for_text_to_disappear(text, timeout: 10)
|
26
|
+
end
|
27
|
+
|
28
|
+
Then /^我看不见"([^\"]*)"$/ do |text|
|
29
|
+
wait_for_text_to_disappear(text, timeout: 10)
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Then /^长按"([^\"]*)"并选择第(\d+)个文本$/ do |text, index|
|
2
|
+
step_deprecated
|
3
|
+
|
4
|
+
long_press_when_element_exists("* {text CONTAINS[c] '#{text}'}")
|
5
|
+
tap_when_element_exists("com.android.internal.view.menu.ListMenuItemView android.widget.TextView index:#{index.to_i - 1}")
|
6
|
+
end
|
7
|
+
|
8
|
+
Then /^长按"([^\"]*)"并选择文本"([^\"]*)"$/ do |text, identifier|
|
9
|
+
step_deprecated
|
10
|
+
|
11
|
+
long_press_when_element_exists("* {text CONTAINS[c] '#{text}'}")
|
12
|
+
tap_when_element_exists("com.android.internal.view.menu.ListMenuItemView android.widget.TextView marked:'#{identifier}'")
|
13
|
+
end
|
14
|
+
|
15
|
+
Then /^长按"([^\"]*)"$/ do |text|
|
16
|
+
long_press_when_element_exists("* {text CONTAINS[c] '#{text}'}")
|
17
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
|
2
|
+
Given /^我在第([^\"]*)个日期选择器上选择日期"(\d\d-\d\d-\d\d\d\d)"$/ do |index,date|
|
3
|
+
set_date("android.widget.DatePicker index:#{index.to_i-1}", date)
|
4
|
+
end
|
5
|
+
|
6
|
+
Given /^我将日期选择器的"([^\"]*)"日期设为"(\d\d-\d\d-\d\d\d\d)"$/ do |content_description, date|
|
7
|
+
set_date("android.widget.DatePicker {contentDescription LIKE[c] '#{content_description}'}", date)
|
8
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
Then /^我在文本框"([^\"]*)"输入"([^\"]*)"$/ do |text, content_description|
|
2
|
+
enter_text("android.widget.EditText {contentDescription LIKE[c] '#{content_description}'}", text)
|
3
|
+
end
|
4
|
+
|
5
|
+
Then /^我输入"([^\"]*)"到第(\d+)个输入框$/ do |text, index|
|
6
|
+
enter_text("android.widget.EditText index:#{index.to_i-1}", text)
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^我输入"([^\"]*)"到输入框其id为"([^\"]*)"$/ do |text, id|
|
10
|
+
enter_text("android.widget.EditText id:'#{id}'", text)
|
11
|
+
end
|
12
|
+
|
13
|
+
Then /^我清空编辑框"([^\"]*)"$/ do |identifier|
|
14
|
+
clear_text_in("android.widget.EditText marked:'#{identifier}'}")
|
15
|
+
end
|
16
|
+
|
17
|
+
Then /^我清空第(\d+)个编辑框$/ do |index|
|
18
|
+
clear_text_in("android.widget.EditText index:#{index.to_i-1}")
|
19
|
+
end
|
20
|
+
|
21
|
+
Then /^我清空编辑框其id为"([^\"]*)"$/ do |id|
|
22
|
+
clear_text_in("android.widget.EditText id:'#{id}'")
|
23
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
Then /^我返回$/ do
|
2
|
+
press_back_button
|
3
|
+
end
|
4
|
+
|
5
|
+
Then /^我点击菜单$/ do
|
6
|
+
press_menu_button
|
7
|
+
end
|
8
|
+
|
9
|
+
Then /^我点击回车键$/ do
|
10
|
+
press_user_action_button
|
11
|
+
# Or, possibly, press_enter_button
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
Then /^向左滑动$/ do
|
16
|
+
perform_action('swipe', 'left')
|
17
|
+
end
|
18
|
+
|
19
|
+
Then /^向右滑动$/ do
|
20
|
+
perform_action('swipe', 'right')
|
21
|
+
end
|
22
|
+
|
23
|
+
Then /^我从菜单中选择"([^\"]*)"$/ do |identifier|
|
24
|
+
select_options_menu_item(identifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
Then /^我选择第(\d+)个tab$/ do | tab |
|
28
|
+
touch("android.widget.TabWidget descendant TextView index:#{tab.to_i-1}")
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param - the "tag" associated with the tab, or the text within the tab label
|
32
|
+
Then /^我选择"([^\"]*)"tab$/ do | tab |
|
33
|
+
touch("android.widget.TabWidget descendant TextView {text LIKE[c] '#{tab}'}")
|
34
|
+
end
|
35
|
+
|
36
|
+
Then /^向下滚动$/ do
|
37
|
+
scroll_down
|
38
|
+
end
|
39
|
+
|
40
|
+
Then /^向上滚动$/ do
|
41
|
+
scroll_up
|
42
|
+
end
|
43
|
+
|
44
|
+
Then /^从位置(\d+):(\d+)拖拽到位置(\d+):(\d+)共拖拽(\d+)步$/ do |from_x, from_y, to_x, to_y, steps|
|
45
|
+
perform_action('drag', from_x, to_x, from_y, to_y, steps)
|
46
|
+
end
|
47
|
+
|