appium_lib 8.2.1 → 9.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/android_tests/lib/android/specs/common/device_touchaction.rb +2 -2
- data/appium_lib.gemspec +2 -2
- data/docs/android_docs.md +245 -212
- data/docs/docs.md +17 -15
- data/docs/index_paths.md +2 -2
- data/docs/ios_docs.md +422 -240
- data/docs/ios_xcuitest.md +25 -0
- data/ios_tests/Gemfile +2 -0
- data/ios_tests/appium.txt +2 -1
- data/ios_tests/lib/common.rb +98 -4
- data/ios_tests/lib/ios/specs/common/helper.rb +24 -28
- data/ios_tests/lib/ios/specs/common/patch.rb +1 -1
- data/ios_tests/lib/ios/specs/device/device.rb +17 -11
- data/ios_tests/lib/ios/specs/device/multi_touch.rb +22 -1
- data/ios_tests/lib/ios/specs/device/touch_actions.rb +14 -5
- data/ios_tests/lib/ios/specs/driver.rb +13 -9
- data/ios_tests/lib/ios/specs/ios/element/alert.rb +12 -8
- data/ios_tests/lib/ios/specs/ios/element/button.rb +6 -3
- data/ios_tests/lib/ios/specs/ios/element/text.rb +5 -3
- data/ios_tests/lib/ios/specs/ios/element/textfield.rb +12 -8
- data/ios_tests/lib/ios/specs/ios/helper.rb +9 -3
- data/ios_tests/lib/ios/specs/ios/patch.rb +9 -1
- data/ios_tests/readme.md +3 -2
- data/lib/appium_lib/common/error.rb +5 -0
- data/lib/appium_lib/common/version.rb +2 -2
- data/lib/appium_lib/device/device.rb +7 -1
- data/lib/appium_lib/device/multi_touch.rb +27 -9
- data/lib/appium_lib/device/touch_actions.rb +12 -5
- data/lib/appium_lib/driver.rb +29 -1
- data/lib/appium_lib/ios/element/button.rb +50 -24
- data/lib/appium_lib/ios/element/generic.rb +20 -4
- data/lib/appium_lib/ios/element/text.rb +48 -24
- data/lib/appium_lib/ios/element/textfield.rb +80 -40
- data/lib/appium_lib/ios/helper.rb +107 -33
- data/lib/appium_lib/ios/mobile_methods.rb +1 -0
- data/lib/appium_lib/ios/patch.rb +5 -2
- data/readme.md +1 -0
- data/release_notes.md +10 -0
- metadata +16 -2
@@ -0,0 +1,25 @@
|
|
1
|
+
## XCUITest
|
2
|
+
- Over Appium1.6.0 provides `XCUITest` automation name based on WebDriverAgent.
|
3
|
+
- [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver)
|
4
|
+
- [WebDriverAgent](https://github.com/facebook/WebDriverAgent)
|
5
|
+
- How to migrate XCUITest from UIAutomation
|
6
|
+
- [Migrating your iOS tests from UIAutomation](https://github.com/appium/appium/blob/v1.6.2/docs/en/advanced-concepts/migrating-to-xcuitest.md)
|
7
|
+
|
8
|
+
|
9
|
+
### Elements
|
10
|
+
- supported elements by find_element is:
|
11
|
+
- [appium-xcuitest-driver](https://github.com/appium/appium-xcuitest-driver/blob/master/lib/commands/find.js#L17)
|
12
|
+
- [WebDriverAgent](https://github.com/facebook/WebDriverAgent/blob/8346199212bffceab24192e81bc0118d65132466/WebDriverAgentLib/Commands/FBFindElementCommands.m#L111)
|
13
|
+
- Mapping
|
14
|
+
- https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m#L19
|
15
|
+
|
16
|
+
### XPath
|
17
|
+
- It is better to avoid XPath strategy.
|
18
|
+
- https://github.com/appium/appium/blob/v1.6.2/docs/en/advanced-concepts/migrating-to-xcuitest.md#xpath-locator-strategy
|
19
|
+
> Try not to use XPath locators unless there is absolutely no other alternatives. In general, xpath locators might be times slower, than other types of locators like accessibility id, class name and predicate (up to 100 times slower in some special cases). They are so slow, because xpath location is not natively supported by Apple's XCTest framework.
|
20
|
+
- Improved performance a bit
|
21
|
+
- https://github.com/appium/appium/issues/6842
|
22
|
+
- https://github.com/facebook/WebDriverAgent/issues/306
|
23
|
+
- XPath in WebDriverAgent
|
24
|
+
- https://github.com/facebook/WebDriverAgent/blob/2158a8d0f305549532f1338fe1e4628cfbd53cd9/WebDriverAgentLib/Categories/XCElementSnapshot%2BFBHelpers.m#L57
|
25
|
+
|
data/ios_tests/Gemfile
CHANGED
data/ios_tests/appium.txt
CHANGED
data/ios_tests/lib/common.rb
CHANGED
@@ -18,17 +18,111 @@ end
|
|
18
18
|
def go_to_textfields
|
19
19
|
screen.must_equal catalog
|
20
20
|
wait_true do
|
21
|
-
text('textfield').click
|
21
|
+
UI::Inventory.xcuitest? ? find_element(:name, 'TextFields').click : text('textfield').click
|
22
22
|
screen == 'TextFields' # wait for screen transition
|
23
23
|
end
|
24
|
-
|
25
|
-
screen.must_equal 'TextFields'
|
26
24
|
end
|
27
25
|
|
28
26
|
def screen
|
29
|
-
$driver.find_element(:class,
|
27
|
+
$driver.find_element(:class, UI::Inventory.navbar).name
|
30
28
|
end
|
31
29
|
|
32
30
|
def catalog
|
33
31
|
'UICatalog'
|
34
32
|
end
|
33
|
+
|
34
|
+
module UI
|
35
|
+
module Inventory
|
36
|
+
def self.xcuitest?
|
37
|
+
$driver.automation_name_is_xcuitest?
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.navbar
|
41
|
+
if xcuitest?
|
42
|
+
'XCUIElementTypeNavigationBar'
|
43
|
+
else
|
44
|
+
'UIANavigationBar'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.button
|
49
|
+
if xcuitest?
|
50
|
+
'XCUIElementTypeButton'
|
51
|
+
else
|
52
|
+
'UIAButton'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.static_text
|
57
|
+
if xcuitest?
|
58
|
+
'XCUIElementTypeStaticText'
|
59
|
+
else
|
60
|
+
'UIAStaticText'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.text_field
|
65
|
+
if xcuitest?
|
66
|
+
'XCUIElementTypeTextField'
|
67
|
+
else
|
68
|
+
'UIATextField'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.secure_text_field
|
73
|
+
if xcuitest?
|
74
|
+
'XCUIElementTypeSecureTextField'
|
75
|
+
else
|
76
|
+
'UIASecureTextField'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.picker
|
81
|
+
if xcuitest?
|
82
|
+
'XCUIElementTypePicker'
|
83
|
+
else
|
84
|
+
'UIAPicker'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.action_sheet
|
89
|
+
if xcuitest?
|
90
|
+
'XCUIElementTypeActionSheet'
|
91
|
+
else
|
92
|
+
'UIActionSheet'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.table
|
97
|
+
if xcuitest?
|
98
|
+
'XCUIElementTypeTable'
|
99
|
+
else
|
100
|
+
'UIATable'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.table_cell
|
105
|
+
if xcuitest?
|
106
|
+
'XCUIElementTypeCell'
|
107
|
+
else
|
108
|
+
'UIATableCell'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.other
|
113
|
+
if xcuitest?
|
114
|
+
'XCUIElementTypeOther'
|
115
|
+
else
|
116
|
+
fail 'unknown UIA element: other'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.status_bar
|
121
|
+
if xcuitest?
|
122
|
+
'XCUIElementTypeStatusBar'
|
123
|
+
else
|
124
|
+
'UIAStatusBar'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -70,18 +70,18 @@ describe 'common/helper.rb' do
|
|
70
70
|
|
71
71
|
t 'back' do
|
72
72
|
# start page
|
73
|
-
tag(
|
73
|
+
tag(UI::Inventory.navbar).name.must_equal 'UICatalog'
|
74
74
|
# nav to new page.
|
75
75
|
wait_true do
|
76
76
|
text('buttons').click
|
77
|
-
tag(
|
77
|
+
tag(UI::Inventory.navbar).name == 'Buttons'
|
78
78
|
end
|
79
79
|
|
80
|
-
tag(
|
80
|
+
tag(UI::Inventory.navbar).name.must_equal 'Buttons'
|
81
81
|
# go back
|
82
82
|
back_click
|
83
83
|
# start page
|
84
|
-
tag(
|
84
|
+
tag(UI::Inventory.navbar).name.must_equal 'UICatalog'
|
85
85
|
end
|
86
86
|
|
87
87
|
t 'session_id' do
|
@@ -90,11 +90,11 @@ describe 'common/helper.rb' do
|
|
90
90
|
end
|
91
91
|
|
92
92
|
t 'xpath' do
|
93
|
-
xpath(
|
93
|
+
xpath("//#{UI::Inventory.static_text}").name.must_equal 'UICatalog'
|
94
94
|
end
|
95
95
|
|
96
96
|
t 'xpaths' do
|
97
|
-
xpaths(
|
97
|
+
xpaths("//#{UI::Inventory.static_text}").length.must_equal 25
|
98
98
|
end
|
99
99
|
|
100
100
|
def uibutton_text
|
@@ -102,13 +102,13 @@ describe 'common/helper.rb' do
|
|
102
102
|
end
|
103
103
|
|
104
104
|
t 'ele_index' do
|
105
|
-
ele_index(
|
105
|
+
ele_index(UI::Inventory.static_text, 2).name.must_equal uibutton_text
|
106
106
|
end
|
107
107
|
|
108
108
|
# TODO: 'string_attr_exact'
|
109
109
|
|
110
110
|
t 'find_ele_by_attr' do
|
111
|
-
el_id = find_ele_by_attr(
|
111
|
+
el_id = find_ele_by_attr(UI::Inventory.static_text, 'name', uibutton_text).instance_variable_get :@id
|
112
112
|
el_id.must_match(/\d+/)
|
113
113
|
end
|
114
114
|
|
@@ -117,10 +117,10 @@ describe 'common/helper.rb' do
|
|
117
117
|
# no space after the !
|
118
118
|
set_wait 1
|
119
119
|
# empty array returned when no match
|
120
|
-
found = !find_eles_by_attr(
|
120
|
+
found = !find_eles_by_attr(UI::Inventory.static_text, 'name', uibutton_text).empty?
|
121
121
|
found.must_equal true
|
122
122
|
|
123
|
-
found = !find_eles_by_attr(
|
123
|
+
found = !find_eles_by_attr(UI::Inventory.static_text, 'name', 'zz').empty?
|
124
124
|
found.must_equal false
|
125
125
|
set_wait
|
126
126
|
end
|
@@ -128,26 +128,29 @@ describe 'common/helper.rb' do
|
|
128
128
|
# TODO: 'string_attr_include'
|
129
129
|
|
130
130
|
t 'find_ele_by_attr_include' do
|
131
|
-
el_text = find_ele_by_attr_include(
|
131
|
+
el_text = find_ele_by_attr_include(UI::Inventory.static_text, :name, 'button').text
|
132
132
|
el_text.must_equal uibutton_text
|
133
133
|
|
134
|
-
el_name = find_ele_by_attr_include(
|
134
|
+
el_name = find_ele_by_attr_include(UI::Inventory.static_text, :name, 'button').name
|
135
135
|
el_name.must_equal uibutton_text
|
136
136
|
end
|
137
137
|
|
138
138
|
t 'find_eles_by_attr_include' do
|
139
|
-
ele_count = find_eles_by_attr_include(
|
140
|
-
|
139
|
+
ele_count = find_eles_by_attr_include(UI::Inventory.static_text, :name, 'e').length
|
140
|
+
expected = UI::Inventory.xcuitest? ? 20 : 19
|
141
|
+
ele_count.must_equal expected
|
141
142
|
end
|
142
143
|
|
143
144
|
t 'first_ele' do
|
144
|
-
first_ele(
|
145
|
+
first_ele(UI::Inventory.static_text).name.must_equal 'UICatalog'
|
145
146
|
end
|
146
147
|
|
147
148
|
t 'last_ele' do
|
148
|
-
|
149
|
-
|
150
|
-
el.
|
149
|
+
expected = UI::Inventory.xcuitest? ? 'Shows UIViewAnimationTransitions' : 'Transitions'
|
150
|
+
|
151
|
+
el = last_ele(UI::Inventory.static_text)
|
152
|
+
el.text.must_equal expected
|
153
|
+
el.name.must_equal expected
|
151
154
|
end
|
152
155
|
|
153
156
|
# t 'source' do # tested by get_source
|
@@ -156,10 +159,6 @@ describe 'common/helper.rb' do
|
|
156
159
|
get_source.class.must_equal String
|
157
160
|
end
|
158
161
|
|
159
|
-
t 'id' do
|
160
|
-
id 'ButtonsExplain' # 'Various uses of UIButton'
|
161
|
-
end
|
162
|
-
|
163
162
|
t 'invalid id should error' do
|
164
163
|
proc { id 'does not exist' }.must_raise Selenium::WebDriver::Error::NoSuchElementError
|
165
164
|
|
@@ -168,15 +167,15 @@ describe 'common/helper.rb' do
|
|
168
167
|
end
|
169
168
|
|
170
169
|
t 'tag' do
|
171
|
-
tag(
|
170
|
+
tag(UI::Inventory.navbar).name.must_equal 'UICatalog'
|
172
171
|
end
|
173
172
|
|
174
173
|
t 'tags' do
|
175
|
-
tags(
|
174
|
+
tags(UI::Inventory.table_cell).length.must_equal 12
|
176
175
|
end
|
177
176
|
|
178
177
|
t 'find_eles_by_attr_include' do
|
179
|
-
find_eles_by_attr_include(
|
178
|
+
find_eles_by_attr_include(UI::Inventory.static_text, 'name', 'Use').length.must_equal 7
|
180
179
|
end
|
181
180
|
|
182
181
|
t 'get_page_class' do
|
@@ -185,10 +184,7 @@ describe 'common/helper.rb' do
|
|
185
184
|
end
|
186
185
|
|
187
186
|
# TODO: write tests
|
188
|
-
# get_page_class
|
189
187
|
# page_class
|
190
|
-
# tag
|
191
|
-
# tags
|
192
188
|
# px_to_window_rel
|
193
189
|
# lazy_load_strings
|
194
190
|
# xml_keys
|
@@ -18,8 +18,10 @@ describe 'device/device' do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
t 'lock' do
|
21
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
22
|
+
|
21
23
|
lock 5
|
22
|
-
tag(
|
24
|
+
tag(UI::Inventory.button).name.must_equal 'SlideToUnlock'
|
23
25
|
|
24
26
|
# It appears that lockForDuration doesn't.
|
25
27
|
close_app
|
@@ -32,18 +34,22 @@ describe 'device/device' do
|
|
32
34
|
end
|
33
35
|
|
34
36
|
t 'app_installed' do
|
37
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
38
|
+
|
35
39
|
installed = app_installed? 'Derrp'
|
36
40
|
installed.must_equal false
|
37
41
|
end
|
38
42
|
|
39
43
|
t 'shake' do
|
44
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
45
|
+
|
40
46
|
shake
|
41
47
|
end
|
42
48
|
|
43
49
|
t 'close and launch' do
|
44
50
|
close_app
|
45
51
|
launch_app
|
46
|
-
tag(
|
52
|
+
tag(UI::Inventory.navbar).name.must_equal 'UICatalog'
|
47
53
|
end
|
48
54
|
|
49
55
|
t 'reset' do
|
@@ -75,23 +81,23 @@ describe 'device/device' do
|
|
75
81
|
end
|
76
82
|
|
77
83
|
t 'swipe' do
|
78
|
-
swipe start_x: 75, start_y: 500, delta_x:
|
79
|
-
end
|
80
|
-
|
81
|
-
t 'pinch & zoom' do
|
82
|
-
wait { id('ImagesExplain').click }
|
83
|
-
# both of these appear to do nothing on iOS 8
|
84
|
-
zoom 200
|
85
|
-
pinch 75
|
86
|
-
go_back
|
84
|
+
swipe start_x: 75, start_y: 500, delta_x: 0, delta_y: -500, duration: 800
|
87
85
|
end
|
88
86
|
|
89
87
|
t 'pull_file' do
|
88
|
+
# Selenium::WebDriver::Error::UnknownError: An unknown server-side error occurred while processing the command.
|
89
|
+
# Original error: Cannot read property 'getDir' of undefined
|
90
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
91
|
+
|
90
92
|
read_file = pull_file 'Library/AddressBook/AddressBook.sqlitedb'
|
91
93
|
read_file.start_with?('SQLite format').must_equal true
|
92
94
|
end
|
93
95
|
|
94
96
|
t 'pull_folder' do
|
97
|
+
# Selenium::WebDriver::Error::UnknownError: An unknown server-side error occurred while processing the command.
|
98
|
+
# Original error: Cannot read property 'getDir' of undefined
|
99
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
100
|
+
|
95
101
|
data = pull_folder 'Library/AddressBook'
|
96
102
|
data.length.must_be :>, 1
|
97
103
|
end
|
@@ -1,5 +1,26 @@
|
|
1
|
+
# rake ios[device/multi_touch]
|
1
2
|
describe 'device/multi_touch' do
|
2
|
-
|
3
|
+
def before_first
|
4
|
+
screen.must_equal catalog
|
5
|
+
end
|
6
|
+
|
7
|
+
# go back to the main page
|
8
|
+
def go_back
|
9
|
+
back
|
10
|
+
wait { !exists { id 'ArrowButton' } } # successfully transitioned back
|
11
|
+
end
|
12
|
+
|
13
|
+
t 'before_first' do
|
14
|
+
before_first
|
15
|
+
end
|
16
|
+
|
17
|
+
t 'pinch & zoom' do
|
18
|
+
wait { id('Images').click }
|
19
|
+
# both of these appear to do nothing on iOS 8
|
20
|
+
zoom 200
|
21
|
+
pinch 75
|
22
|
+
go_back
|
23
|
+
end
|
3
24
|
end
|
4
25
|
|
5
26
|
# TODO: write tests
|
@@ -1,18 +1,27 @@
|
|
1
|
+
# rake ios[device/touch_actions]
|
1
2
|
describe 'device/touch_actions' do
|
3
|
+
def after_last
|
4
|
+
back_click
|
5
|
+
end
|
6
|
+
|
2
7
|
t 'swipe_default_duration' do
|
3
8
|
wait_true do
|
4
|
-
text('pickers').click
|
9
|
+
wait { UI::Inventory.xcuitest? ? find_element(:name, 'Pickers').click : text('pickers').click }
|
5
10
|
screen == 'Pickers'
|
6
11
|
end
|
7
12
|
|
8
|
-
ele_index(
|
9
|
-
picker = ele_index(
|
13
|
+
ele_index(UI::Inventory.static_text, 2).text.must_equal 'John Appleseed - 0'
|
14
|
+
picker = ele_index(UI::Inventory.picker, 1)
|
10
15
|
loc = picker.location.to_h
|
11
16
|
size = picker.size.to_h
|
12
17
|
start_x = loc[:x] + size[:width] / 2
|
13
18
|
start_y = loc[:y] + size[:height] / 2
|
14
|
-
swipe start_x: start_x, start_y: start_y, delta_x:
|
15
|
-
ele_index(
|
19
|
+
swipe start_x: start_x, start_y: start_y, delta_x: 0, delta_y: - 50
|
20
|
+
ele_index(UI::Inventory.static_text, 2).text.must_equal 'Chris Armstrong - 0'
|
21
|
+
end
|
22
|
+
|
23
|
+
t 'after_last' do
|
24
|
+
after_last
|
16
25
|
end
|
17
26
|
end
|
18
27
|
|
@@ -36,7 +36,8 @@ describe 'driver' do
|
|
36
36
|
actual = driver_attributes
|
37
37
|
actual[:caps][:app] = File.basename actual[:caps][:app]
|
38
38
|
expected = { caps: { platformName: 'ios',
|
39
|
-
platformVersion: '
|
39
|
+
platformVersion: '10.1',
|
40
|
+
automationName: 'XCUITest',
|
40
41
|
deviceName: 'iPhone Simulator',
|
41
42
|
app: 'UICatalog.app' },
|
42
43
|
custom_url: false,
|
@@ -145,7 +146,11 @@ describe 'driver' do
|
|
145
146
|
end
|
146
147
|
|
147
148
|
t 'driver' do
|
148
|
-
driver.browser.
|
149
|
+
driver.browser.must_be_empty
|
150
|
+
end
|
151
|
+
|
152
|
+
t 'automation_name_is_xcuitest?' do
|
153
|
+
automation_name_is_xcuitest?.must_equal UI::Inventory.xcuitest?
|
149
154
|
end
|
150
155
|
|
151
156
|
#
|
@@ -188,30 +193,29 @@ describe 'driver' do
|
|
188
193
|
|
189
194
|
# simple integration sanity test to check for unexpected exceptions
|
190
195
|
t 'set_location' do
|
196
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
191
197
|
set_location latitude: 55, longitude: -72, altitude: 33
|
192
198
|
end
|
193
199
|
|
194
|
-
# any script
|
195
|
-
t 'execute_script' do
|
196
|
-
execute_script %q(au.mainApp().getFirstWithPredicate("name contains[c] 'button'");)
|
197
|
-
end
|
198
|
-
|
199
200
|
# any elements
|
200
201
|
t 'find_elements' do
|
201
|
-
find_elements(:class,
|
202
|
+
find_elements(:class, UI::Inventory.table_cell).length.must_equal 12
|
202
203
|
end
|
203
204
|
|
204
205
|
# any element
|
205
206
|
t 'find_element' do
|
206
|
-
find_element(:class,
|
207
|
+
find_element(:class, UI::Inventory.static_text).class.must_equal Selenium::WebDriver::Element
|
207
208
|
end
|
208
209
|
|
209
210
|
# settings
|
210
211
|
t 'get settings' do
|
212
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
211
213
|
get_settings.wont_be_nil
|
212
214
|
end
|
213
215
|
|
214
216
|
t 'update settings' do
|
217
|
+
fail NotImplementedError, "XCUITest(Appium1.6.2) doesn't support yet" if UI::Inventory.xcuitest?
|
218
|
+
|
215
219
|
update_settings cyberdelia: 'open'
|
216
220
|
get_settings['cyberdelia'].must_equal 'open'
|
217
221
|
end
|