tabbyx 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|