testcentricity_web 3.0.9 → 3.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.md CHANGED
@@ -1,4 +1,4 @@
1
- TestCentricity™ Web Framework is Copyright (c) 2014-2018, Tony Mrozinski
1
+ Copyright (c) 2011-2018, 2014-2018, Tony Mrozinski
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without
@@ -24,4 +24,4 @@ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
24
24
  OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
25
  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
26
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
- POSSIBILITY OF SUCH DAMAGE.
27
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -24,7 +24,15 @@ The TestCentricity™ Web gem supports running automated tests against the follo
24
24
 
25
25
  ## What's New
26
26
 
27
- A complete history of bug fixes and new features can be found in the {file:HISTORY.md HISTORY} file.
27
+ A complete history of bug fixes and new features can be found in the {file:CHANGELOG.md CHANGELOG} file.
28
+
29
+ ###Version 3.0.10
30
+
31
+ * Added `Image.broken?` method.
32
+ * Added `UIElement.highlight` and `UIElement.unhighlight` methods.
33
+ * `PageObject.verify_ui_states` and `PageSection.verify_ui_states` methods now takes screenshots that display a red dashed rectangular highlight around
34
+ any UI element with property states that do not match expected results.
35
+ * Removed deprecated `DataObject.set_current` method.
28
36
 
29
37
  ###Version 3.0.9
30
38
 
@@ -1550,21 +1558,27 @@ landscape orientation running on the BrowserStack service:
1550
1558
  TestCentricity™ Framework is Copyright (c) 2014-2018, Tony Mrozinski.
1551
1559
  All rights reserved.
1552
1560
 
1553
-
1554
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
1555
- are met:
1556
-
1557
- 1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer.
1558
-
1559
- 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in
1560
- the documentation and/or other materials provided with the distribution.
1561
-
1562
- 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from
1563
- this software without specific prior written permission.
1564
-
1565
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
1566
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
1567
- HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
1568
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
1569
- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
1570
- USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1561
+ Redistribution and use in source and binary forms, with or without
1562
+ modification, are permitted provided that the following conditions are met:
1563
+
1564
+ 1. Redistributions of source code must retain the above copyright notice,
1565
+ this list of conditions and the following disclaimer.
1566
+
1567
+ 2. Redistributions in binary form must reproduce the above copyright
1568
+ notice, this list of conditions and the following disclaimer in the
1569
+ documentation and/or other materials provided with the distribution.
1570
+
1571
+ 3. Neither the name of the copyright holder nor the names of its contributors
1572
+ may be used to endorse or promote products derived from this software without
1573
+ specific prior written permission.
1574
+
1575
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1576
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1577
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1578
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
1579
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
1580
+ NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
1581
+ OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
1582
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
1583
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
1584
+ POSSIBILITY OF SUCH DAMAGE.
@@ -17,12 +17,6 @@ module TestCentricity
17
17
  @hash_table = data
18
18
  end
19
19
 
20
- # @deprecated Please use {#current=} instead
21
- def self.set_current(current)
22
- warn "[DEPRECATION] 'TestCentricity::DataObject.set_current' is deprecated. Please use 'current=' instead."
23
- @current = current
24
- end
25
-
26
20
  def self.current
27
21
  @current
28
22
  end
@@ -34,45 +28,73 @@ module TestCentricity
34
28
 
35
29
 
36
30
  class DataSource
37
- attr_accessor :current
31
+ attr_accessor :file_path
32
+ attr_accessor :node
38
33
 
39
34
  def read_yaml_node_data(file_name, node_name)
40
- data = YAML.load_file("#{XL_PRIMARY_DATA_PATH}#{file_name}")
35
+ @file_path = "#{XL_PRIMARY_DATA_PATH}#{file_name}"
36
+ @node = node_name
37
+ data = YAML.load_file(@file_path)
41
38
  data[node_name]
42
39
  end
43
40
 
41
+ def write_yaml_node_data(file_name, node_name, node_data)
42
+ data = read_yaml_node_data(file_name, node_name)
43
+ data[node_name] = node_data
44
+ File.write(@file_path, data.to_yaml)
45
+ end
46
+
44
47
  def read_json_node_data(file_name, node_name)
45
- raw_data = File.read("#{XL_PRIMARY_DATA_PATH}#{file_name}")
48
+ @file_path = "#{XL_PRIMARY_DATA_PATH}#{file_name}"
49
+ @node = node_name
50
+ raw_data = File.read(@file_path)
46
51
  data = JSON.parse(raw_data)
47
52
  data[node_name]
48
53
  end
54
+
55
+ def write_json_node_data(file_name, node_name, node_data)
56
+ data = read_json_node_data(file_name, node_name)
57
+ data[node_name] = node_data
58
+ File.write(@file_path, data.to_json)
59
+ end
49
60
  end
50
61
 
51
62
 
52
63
  class ExcelDataSource < TestCentricity::DataSource
64
+ attr_accessor :worksheet
65
+ attr_accessor :row_spec
66
+
53
67
  def pick_excel_data_source(sheet, row_spec)
68
+ @worksheet = sheet
54
69
  if ENV['TEST_ENVIRONMENT']
55
70
  environment = ENV['TEST_ENVIRONMENT']
56
71
  data_file = "#{XL_PRIMARY_DATA_PATH}#{environment}_data.xls"
57
- data_file = XL_PRIMARY_DATA_FILE unless ExcelData.rowspec_exists?(data_file, sheet, row_spec)
72
+ data_file = XL_PRIMARY_DATA_FILE unless ExcelData.row_spec_exists?(data_file, @worksheet, row_spec)
58
73
  else
59
74
  data_file = XL_PRIMARY_DATA_FILE
60
75
  end
76
+ @file_path = data_file
61
77
  data_file
62
78
  end
63
79
 
64
80
  def read_excel_row_data(sheet, row_name, parallel = false)
65
- parallel == :parallel && ENV['PARALLEL'] ? row_spec = "#{row_name}#{ENV['TEST_ENV_NUMBER']}" : row_spec = row_name
66
- ExcelData.read_row_data(pick_excel_data_source(sheet, row_spec), sheet, row_spec)
81
+ @row_spec = parallel == :parallel && ENV['PARALLEL'] ? "#{row_name}#{ENV['TEST_ENV_NUMBER']}" : row_name
82
+ ExcelData.read_row_data(pick_excel_data_source(sheet, @row_spec), sheet, @row_spec)
67
83
  end
68
84
 
69
85
  def read_excel_pool_data(sheet, row_name, parallel = false)
70
- parallel == :parallel && ENV['PARALLEL'] ? row_spec = "#{row_name}#{ENV['TEST_ENV_NUMBER']}" : row_spec = row_name
71
- ExcelData.read_row_from_pool(pick_excel_data_source(sheet, row_name), sheet, row_spec)
86
+ @row_spec = parallel == :parallel && ENV['PARALLEL'] ? "#{row_name}#{ENV['TEST_ENV_NUMBER']}" : row_name
87
+ ExcelData.read_row_from_pool(pick_excel_data_source(sheet, row_name), sheet, @row_spec)
72
88
  end
73
89
 
74
90
  def read_excel_range_data(sheet, range_name)
91
+ @row_spec = range_name
75
92
  ExcelData.read_range_data(pick_excel_data_source(sheet, range_name), sheet, range_name)
76
93
  end
94
+
95
+ def write_excel_row_data(sheet, row_name, row_data, parallel = false)
96
+ @row_spec = parallel == :parallel && ENV['PARALLEL'] ? "#{row_name}#{ENV['TEST_ENV_NUMBER']}" : row_name
97
+ ExcelData.write_row_data(pick_excel_data_source(sheet, @row_spec), sheet, @row_spec, row_data)
98
+ end
77
99
  end
78
100
  end
@@ -11,7 +11,7 @@ module TestCentricity
11
11
  def self.worksheet_exists?(file, sheet)
12
12
  exists = false
13
13
  if File.exist?(file)
14
- work_book = Spreadsheet.open file
14
+ work_book = Spreadsheet.open(file)
15
15
  worksheets = work_book.worksheets
16
16
  worksheets.each do |worksheet|
17
17
  if worksheet.name == sheet
@@ -23,15 +23,15 @@ module TestCentricity
23
23
  exists
24
24
  end
25
25
 
26
- def self.rowspec_exists?(file, sheet, rowspec)
26
+ def self.row_spec_exists?(file, sheet, row_spec)
27
27
  exists = false
28
28
  if worksheet_exists?(file, sheet)
29
- work_book = Spreadsheet.open file
30
- work_sheet = work_book.worksheet sheet
29
+ work_book = Spreadsheet.open(file)
30
+ work_sheet = work_book.worksheet(sheet)
31
31
  # get column headings from row 0 of worksheet
32
32
  headings = work_sheet.row(0)
33
- # if rowspec is a string then we have to find a matching row name
34
- if rowspec.is_a? String
33
+ # if row_spec is a string then we have to find a matching row name
34
+ if row_spec.is_a? String
35
35
  column_number = 0
36
36
  exists = false
37
37
  headings.each do |heading|
@@ -42,27 +42,25 @@ module TestCentricity
42
42
  column_number += 1
43
43
  end
44
44
  raise "Could not find a column named ROW_NAME in worksheet #{sheet}" unless exists
45
- # find first cell in ROW_NAME column containing a string that matches the rowspec parameter
45
+ # find first cell in ROW_NAME column containing a string that matches the row_spec parameter
46
46
  exists = false
47
- row_number = 0
48
47
  work_sheet.each do |row|
49
- if row[column_number] == rowspec
48
+ if row[column_number] == row_spec
50
49
  exists = true
51
50
  break
52
51
  end
53
- row_number += 1
54
52
  end
55
53
  end
56
54
  end
57
55
  exists
58
56
  end
59
57
 
60
- def self.read_row_from_pool(file, sheet, rowspec, columns = nil)
58
+ def self.read_row_from_pool(file, sheet, row_spec, columns = nil)
61
59
  raise "File #{file} does not exists" unless File.exist?(file)
62
- work_book = Spreadsheet.open file
63
- work_sheet = work_book.worksheet sheet
60
+ work_book = Spreadsheet.open(file)
61
+ work_sheet = work_book.worksheet(sheet)
64
62
 
65
- pool_spec_key = "#{sheet}:#{rowspec}"
63
+ pool_spec_key = "#{sheet}:#{row_spec}"
66
64
  if @mru.key?(pool_spec_key)
67
65
  pool_spec = @mru[pool_spec_key]
68
66
  row_start = pool_spec[:start_row]
@@ -81,9 +79,9 @@ module TestCentricity
81
79
  end
82
80
 
83
81
  pool_spec = {
84
- start_row: row_start,
85
- num_rows: row_end,
86
- used_rows: mru_rows
82
+ start_row: row_start,
83
+ num_rows: row_end,
84
+ used_rows: mru_rows
87
85
  }
88
86
  else
89
87
  # get column headings from row 0 of worksheet
@@ -98,24 +96,24 @@ module TestCentricity
98
96
  column_number += 1
99
97
  end
100
98
  raise "Could not find a column named ROW_NAME in worksheet #{sheet}" unless found
101
- # find cell(s) in ROW_NAME column containing a string that matches the rowspec parameter
99
+ # find cell(s) in ROW_NAME column containing a string that matches the row_spec parameter
102
100
  found = []
103
101
  row_number = 0
104
102
  work_sheet.each do |row|
105
- if row[column_number] == rowspec
103
+ if row[column_number] == row_spec
106
104
  found.push(row_number)
107
105
  elsif !found.empty?
108
106
  break
109
107
  end
110
108
  row_number += 1
111
109
  end
112
- raise "Could not find a row named '#{rowspec}' in worksheet #{sheet}" if found.empty?
110
+ raise "Could not find a row named '#{row_spec}' in worksheet #{sheet}" if found.empty?
113
111
 
114
112
  new_row = found.sample.to_i
115
113
  pool_spec = {
116
- start_row: found[0],
117
- num_rows: found.size,
118
- used_rows: [new_row]
114
+ start_row: found[0],
115
+ num_rows: found.size,
116
+ used_rows: [new_row]
119
117
  }
120
118
  end
121
119
  @mru[pool_spec_key] = pool_spec
@@ -123,14 +121,14 @@ module TestCentricity
123
121
  read_row_data(file, sheet, new_row, columns)
124
122
  end
125
123
 
126
- def self.read_row_data(file, sheet, rowspec, columns = nil)
124
+ def self.read_row_data(file, sheet, row_spec, columns = nil)
127
125
  raise "File #{file} does not exists" unless File.exist?(file)
128
- work_book = Spreadsheet.open file
129
- work_sheet = work_book.worksheet sheet
126
+ work_book = Spreadsheet.open(file)
127
+ work_sheet = work_book.worksheet(sheet)
130
128
  # get column headings from row 0 of worksheet
131
129
  headings = work_sheet.row(0)
132
- # if rowspec is a string then we have to find a matching row name
133
- if rowspec.is_a? String
130
+ # if row_spec is a string then we have to find a matching row name
131
+ if row_spec.is_a? String
134
132
  column_number = 0
135
133
  found = false
136
134
  headings.each do |heading|
@@ -141,22 +139,22 @@ module TestCentricity
141
139
  column_number += 1
142
140
  end
143
141
  raise "Could not find a column named ROW_NAME in worksheet #{sheet}" unless found
144
- # find first cell in ROW_NAME column containing a string that matches the rowspec parameter
142
+ # find first cell in ROW_NAME column containing a string that matches the row_spec parameter
145
143
  found = false
146
144
  row_number = 0
147
145
  work_sheet.each do |row|
148
- if row[column_number] == rowspec
146
+ if row[column_number] == row_spec
149
147
  found = true
150
148
  break
151
149
  end
152
150
  row_number += 1
153
151
  end
154
- raise "Could not find a row named '#{rowspec}' in worksheet #{sheet}" unless found
152
+ raise "Could not find a row named '#{row_spec}' in worksheet #{sheet}" unless found
155
153
  data = work_sheet.row(row_number)
156
- # if rowspec is a number then ensure that it doesn't exceed the number of available rows
157
- elsif rowspec.is_a? Numeric
158
- raise "Row # #{rowspec} is greater than number of rows in worksheet #{sheet}" if rowspec > work_sheet.last_row_index
159
- data = work_sheet.row(rowspec)
154
+ # if row_spec is a number then ensure that it doesn't exceed the number of available rows
155
+ elsif row_spec.is_a? Numeric
156
+ raise "Row # #{row_spec} is greater than number of rows in worksheet #{sheet}" if row_spec > work_sheet.last_row_index
157
+ data = work_sheet.row(row_spec)
160
158
  end
161
159
 
162
160
  # if no columns have been specified, return all columns
@@ -181,10 +179,10 @@ module TestCentricity
181
179
  result
182
180
  end
183
181
 
184
- def self.read_range_data(file, sheet, rangespec)
182
+ def self.read_range_data(file, sheet, range_spec)
185
183
  raise "File #{file} does not exists" unless File.exist?(file)
186
- work_book = Spreadsheet.open file
187
- work_sheet = work_book.worksheet sheet
184
+ work_book = Spreadsheet.open(file)
185
+ work_sheet = work_book.worksheet(sheet)
188
186
  # get column headings from row 0 of worksheet
189
187
  headings = work_sheet.row(0)
190
188
  column_number = 0
@@ -197,18 +195,18 @@ module TestCentricity
197
195
  column_number += 1
198
196
  end
199
197
  raise "Could not find a column named ROW_NAME in worksheet #{sheet}" unless found
200
- # find cell(s) in ROW_NAME column containing a string that matches the rangespec parameter
198
+ # find cell(s) in ROW_NAME column containing a string that matches the range_spec parameter
201
199
  found = []
202
200
  row_number = 0
203
201
  work_sheet.each do |row|
204
- if row[column_number] == rangespec
202
+ if row[column_number] == range_spec
205
203
  found.push(row_number)
206
204
  elsif !found.empty?
207
205
  break
208
206
  end
209
207
  row_number += 1
210
208
  end
211
- raise "Could not find a row named '#{rangespec}' in worksheet #{sheet}" if found.empty?
209
+ raise "Could not find a row named '#{range_spec}' in worksheet #{sheet}" if found.empty?
212
210
 
213
211
  result = []
214
212
  found.each do |row|
@@ -217,6 +215,65 @@ module TestCentricity
217
215
  result
218
216
  end
219
217
 
218
+ def self.write_row_data(file, sheet, row_spec, row_data)
219
+ raise "File #{file} does not exists" unless File.exist?(file)
220
+ work_book = Spreadsheet.open(file)
221
+ work_sheet = work_book.worksheet(sheet)
222
+ # get column headings from row 0 of worksheet
223
+ headings = work_sheet.row(0)
224
+ # find a matching row name
225
+ column_number = 0
226
+ found = false
227
+ headings.each do |heading|
228
+ if heading == 'ROW_NAME'
229
+ found = true
230
+ break
231
+ end
232
+ column_number += 1
233
+ end
234
+ raise "Could not find a column named ROW_NAME in worksheet #{sheet}" unless found
235
+ # find first cell in ROW_NAME column containing a string that matches the row_spec parameter
236
+ found = false
237
+ row_number = 0
238
+ work_sheet.each do |row|
239
+ if row[column_number] == row_spec
240
+ found = true
241
+ break
242
+ end
243
+ row_number += 1
244
+ end
245
+ raise "Could not find a row named '#{row_spec}' in worksheet #{sheet}" unless found
246
+ # iterate through the row_data Hash
247
+ row_data.each do |column, value|
248
+ column_number = 0
249
+ found = false
250
+ # find the column heading that matches the specified column name
251
+ headings.each do |heading|
252
+ if heading == column
253
+ found = true
254
+ break
255
+ end
256
+ column_number += 1
257
+ end
258
+ raise "Could not find a column named '#{column}' in worksheet #{sheet}" unless found
259
+ # set the value of the specified row and column
260
+ work_sheet.rows[row_number][column_number] = value
261
+ end
262
+ # iterate through all worksheets so that all worksheets are saved in new Excel document
263
+ worksheets = work_book.worksheets
264
+ worksheets.each do |worksheet|
265
+ headings = worksheet.row(0)
266
+ worksheet.rows[0][0] = headings[0]
267
+ end
268
+ # write all changes to new Excel document
269
+ outfile = file.gsub(File.basename(file), 'output.xls')
270
+ work_book.write outfile
271
+ # delete original Excel document
272
+ File.delete(file)
273
+ # rename new Excel document, replacing the original
274
+ File.rename(outfile, file)
275
+ end
276
+
220
277
  private
221
278
 
222
279
  def self.calculate_dynamic_value(value)
@@ -230,11 +287,11 @@ module TestCentricity
230
287
  date_time = eval("Chronic.parse('#{date_time_params[0].strip}')")
231
288
  result = date_time.to_s.format_date_time("#{date_time_params[1].strip}")
232
289
  else
233
- if Faker.constants.include?(parameter[0].to_sym)
234
- result = eval("Faker::#{parameter[0]}.#{parameter[1]}")
235
- else
236
- result = eval(test_value[1])
237
- end
290
+ result = if Faker.constants.include?(parameter[0].to_sym)
291
+ eval("Faker::#{parameter[0]}.#{parameter[1]}")
292
+ else
293
+ eval(test_value[1])
294
+ end
238
295
  end
239
296
  result.to_s
240
297
  end
@@ -2,7 +2,9 @@ module TestCentricity
2
2
  class ExceptionQueue
3
3
  include Capybara::DSL
4
4
 
5
- @error_queue
5
+ attr_accessor :error_queue
6
+ attr_accessor :active_ui_element
7
+ attr_accessor :mru_ui_element
6
8
 
7
9
  def self.enqueue_assert_equal(expected, actual, error_message)
8
10
  unless expected == actual
@@ -29,9 +31,12 @@ module TestCentricity
29
31
  end
30
32
  ensure
31
33
  @error_queue = nil
34
+ @active_ui_element = nil
35
+ @mru_ui_element = nil
32
36
  end
33
37
 
34
- def self.enqueue_comparison(state, actual, error_msg)
38
+ def self.enqueue_comparison(ui_object, state, actual, error_msg)
39
+ @active_ui_element = ui_object
35
40
  if state.is_a?(Hash) && state.length == 1
36
41
  state.each do |key, value|
37
42
  case key
@@ -94,11 +99,20 @@ module TestCentricity
94
99
  timestamp = Time.now.strftime('%Y%m%d%H%M%S')
95
100
  filename = "Screenshot-#{timestamp}"
96
101
  path = File.join Dir.pwd, 'reports/screenshots/', filename
102
+ # highlight the active UI element prior to taking a screenshot
103
+ unless @active_ui_element.nil? || @mru_ui_element == @active_ui_element
104
+ @active_ui_element.highlight(0)
105
+ @mru_ui_element = @active_ui_element
106
+ end
107
+ # take screenshot
97
108
  if Environ.driver == :appium
98
109
  AppiumConnect.take_screenshot("#{path}.png")
99
110
  else
100
111
  Capybara.save_screenshot "#{path}.png"
101
112
  end
113
+ # unhighlight the active UI element
114
+ @mru_ui_element.unhighlight unless @mru_ui_element.blank?
115
+ # add screenshot to queue
102
116
  puts "Screenshot saved at #{path}.png"
103
117
  screen_shot = {path: path, filename: filename}
104
118
  Environ.save_screen_shot(screen_shot)