lapis_lazuli 3.0.1 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ea6c404b7d1a7a47007360bda7725812af256234c6b33b68626ced98c77b3fd
4
- data.tar.gz: e9dc94d78218d53dd6e8bcc0938d8a08481b9e2bc53da9744619183b1f08e099
3
+ metadata.gz: 6371db0c7cade2ee1594513f572a7c36540be30947f35ec275ca88d8cf29f6f6
4
+ data.tar.gz: 58f488b7d84511ec095b04b585a8cbfc0c39853d232ce0285e963191a1ad3e9a
5
5
  SHA512:
6
- metadata.gz: c637008b61477116811bb63e4436f4a00276073da2ad28a6257df7d4812376701e33565000d8abfca02085e6cd6c6877778cff9563609d8f355fa48853006c71
7
- data.tar.gz: 858ce3df0863b0fcc949959bbc0cfa11f9e2b78d238d02f68bed2a6c4912d881e4e191eb4e193b9a051c5b8452dc0129d0eaa7ce8d445354c648e0e5c1f902fd
6
+ metadata.gz: 867d75be07317b7dff52efc926183e48ecc31322a645714fe4aceab9fa2c611f6e2deac7ce00f5c23fee4484407eed495466342dbc91abf80972d6a9caf30ba6
7
+ data.tar.gz: f7ddb7958fc2a0d87d7f0c8f92132f29851955823faf7df7d2afa0bc0aa02dbf5182359f88b57d8dd5ec796cac25a91fff9d5a132b363d5acbaf8ff1663dc7a9
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # 3.0.0
2
+
3
+ ## Breaking changes
4
+
5
+ * Dropped support for annotation
6
+ * Dropped support for Cucumber `2` and `3`
7
+
8
+ ## Changes
9
+
10
+ * Updated Cucumber support version to `>= 4`
11
+ * Updated Watir support version to `>= 6`
12
+ * Unlocked all other dependencies.
13
+ * Don't act as an _in between_ party starting up a Watir browser anymore by passing all browser starts arguments directly to Watir.
data/README.md CHANGED
@@ -7,6 +7,9 @@ test automation suite development.
7
7
  [![Code Climate](https://codeclimate.com/github/spriteCloud/lapis-lazuli/badges/gpa.svg)](https://codeclimate.com/github/spriteCloud/lapis-lazuli)
8
8
  [![Test Coverage](https://codeclimate.com/github/spriteCloud/lapis-lazuli/badges/coverage.svg)](https://codeclimate.com/github/spriteCloud/lapis-lazuli)
9
9
 
10
+ Tested with Cucumber 4, 5 & 6
11
+ For cucumber 2 and 3, have a look at the version 2 release of Lapis Lazuli.
12
+
10
13
  A lot of functionality is aimed at dealing better with [Watir](http://watir.com/),
11
14
  such as:
12
15
 
data/lapis_lazuli.gemspec CHANGED
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.summary = %q{Cucumber helper functions and scaffolding for easier test automation suite development.}
24
24
  spec.homepage = "https://github.com/spriteCloud/lapis-lazuli"
25
25
  spec.license = "MITNFA"
26
- spec.required_ruby_version = '~> 2'
26
+ spec.required_ruby_version = '>= 2'
27
27
  spec.platform = Gem::Platform::RUBY
28
28
 
29
29
  spec.files = `git ls-files -z`.split("\x0")
@@ -31,23 +31,23 @@ Gem::Specification.new do |spec|
31
31
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- spec.add_development_dependency "bundler", "~> 2.0"
35
- spec.add_development_dependency "rake", "~> 12.3"
36
- spec.add_development_dependency "simplecov", "~> 0.17"
34
+ spec.add_development_dependency "bundler", ">= 2.0"
35
+ spec.add_development_dependency "rake", ">= 12.3"
36
+ spec.add_development_dependency "simplecov", ">= 0.17"
37
37
 
38
- spec.add_dependency "faraday_middleware", "~> 0.13"
39
- spec.add_dependency "faraday_json", "~> 0.1"
40
- spec.add_dependency "multi_xml", "~> 0.6"
41
- spec.add_dependency "teelogger", "~> 0.5"
42
- spec.add_dependency "minitest", "~> 5.11"
43
- spec.add_dependency "thor", "~> 0.20" # Used in the cucumber project generator
44
- spec.add_dependency "facets", "~> 3.1" # Used in the cucumber project generator
45
- spec.add_dependency "deep_merge", "~> 1.2"
38
+ spec.add_dependency "faraday_middleware", ">= 0.13"
39
+ spec.add_dependency "faraday_json", ">= 0.1"
40
+ spec.add_dependency "multi_xml", ">= 0.6"
41
+ spec.add_dependency "teelogger", ">= 0.5"
42
+ spec.add_dependency "minitest", ">= 5.11"
43
+ spec.add_dependency "thor", ">= 0.20" # Used in the cucumber project generator
44
+ spec.add_dependency "facets", ">= 3.1" # Used in the cucumber project generator
45
+ spec.add_dependency "deep_merge", ">= 1.2"
46
46
 
47
47
  # webdriver specifics
48
- spec.add_dependency "selenium-webdriver", ">= 2.0", '< 4'
49
- spec.add_dependency "watir", "~> 6"
50
- spec.add_dependency "ffi", "~> 1.11"
51
- spec.add_dependency "cucumber", ">= 2.0", '< 4.0'
48
+ spec.add_dependency "selenium-webdriver", ">= 2.0"
49
+ spec.add_dependency "watir", ">= 6"
50
+ spec.add_dependency "ffi", ">= 1.11"
51
+ spec.add_dependency "cucumber", ">= 4.0"
52
52
 
53
53
  end
data/lib/lapis_lazuli.rb CHANGED
@@ -34,7 +34,6 @@ require "lapis_lazuli/world/config"
34
34
  require "lapis_lazuli/world/hooks"
35
35
  require "lapis_lazuli/world/variable"
36
36
  require "lapis_lazuli/world/error"
37
- require "lapis_lazuli/world/annotate"
38
37
  require "lapis_lazuli/world/logging"
39
38
  require "lapis_lazuli/world/browser"
40
39
  require "lapis_lazuli/world/api"
@@ -49,7 +48,6 @@ module LapisLazuli
49
48
  include LapisLazuli::WorldModule::Hooks
50
49
  include LapisLazuli::WorldModule::Variable
51
50
  include LapisLazuli::WorldModule::Error
52
- include LapisLazuli::WorldModule::Annotate
53
51
  include LapisLazuli::WorldModule::Logging
54
52
  include LapisLazuli::WorldModule::Browser
55
53
  include LapisLazuli::WorldModule::API
@@ -5,23 +5,7 @@
5
5
  # Copyright (c) 2015 spriteCloud B.V. and other LapisLazuli contributors.
6
6
  # All rights reserved.
7
7
  #
8
-
9
- # Hack for cucumber 2.0.x
10
- begin
11
- module Cucumber
12
- module Core
13
- module Ast
14
- class ExamplesTable
15
- public :example_rows
16
- end # ExamplesTable
17
- end # Ast
18
- end # Core
19
- end # Cucumber
20
- rescue NameError
21
- # Not cucumber 2.0.x
22
- end
23
-
24
-
8
+ #
25
9
  module LapisLazuli
26
10
  ##
27
11
  # Convenience module for dealing with aspects of the cucumber AST. From
@@ -30,131 +14,8 @@ module LapisLazuli
30
14
  ##
31
15
  # Return a unique and human parsable ID for scenarios
32
16
  def scenario_id(scenario)
33
- # For 2.0.x, the best scenario ID is one prefixed by the file name + line
34
- # number, followed by the feature name, scenario name, and table data (if
35
- # applicable).
36
- if is_cucumber_2?(scenario)
37
- id = [scenario.location.to_s]
38
- for i in 0 .. scenario.source.length - 1 do
39
- part = scenario.source[i]
40
- if part.respond_to?(:name)
41
- id << part.name
42
- elsif part.is_a?(Cucumber::Core::Ast::ExamplesTable::Row)
43
- id << part.values.join("|")
44
- end
45
- end
46
- return id
47
- end
48
-
49
- case scenario
50
- when Cucumber::Ast::Scenario
51
- return [
52
- scenario.feature.file,
53
- scenario.name
54
- ]
55
- when Cucumber::Ast::OutlineTable::ExampleRow
56
- return [
57
- scenario.scenario_outline.feature.file,
58
- scenario.scenario_outline.name,
59
- scenario.name
60
- ]
61
- end
62
- end
63
-
64
-
65
- ##
66
- # Tests whether the given scenario object indicates we're using cucumber 2.x
67
- def is_cucumber_2?(scenario)
68
- begin
69
- # The assumption - FIXME perhaps wrong - is that cucumber 1.3.x does not
70
- # have this source array.
71
- return (scenario.respond_to?(:source) and scenario.source.is_a?(Array))
72
- rescue
73
- return false
74
- end
75
- end
76
-
77
-
78
- ##
79
- # Tests whether the scenario object is a single scenario
80
- def is_scenario?(scenario)
81
- begin
82
- # 1.3.x
83
- return scenario.class == Cucumber::Ast::Scenario
84
- rescue
85
- # 2.0.x - everything is a Cucumber::Core::Test::Case
86
- return (not scenario.outline?)
87
- end
17
+ [scenario.id]
88
18
  end
89
19
 
90
-
91
- ##
92
- # Tests whether the scenario object is a table row
93
- def is_table_row?(scenario)
94
- begin
95
- # 1.3.x
96
- return scenario.class == Cucumber::Ast::OutlineTable::ExampleRow
97
- rescue
98
- # 2.0.x - everything is a Cucumber::Core::Test::Case
99
- return scenario.outline?
100
- end
101
- end
102
-
103
-
104
- ##
105
- # Tests whether this scenario is the last scenario of a feature
106
- def is_last_scenario?(scenario)
107
- if is_scenario?(scenario)
108
- begin
109
- # 2.0.x
110
- return (scenario.feature.feature_elements.last.location == scenario.location)
111
- rescue
112
- # 1.3.x
113
- return (scenario.feature.feature_elements.last == scenario)
114
- end
115
-
116
- elsif is_table_row?(scenario)
117
- begin
118
- # 2.0.x
119
-
120
- # We can bail early if this scenario's line is < the last feature
121
- # element's line
122
- outline = scenario.feature.feature_elements.last
123
- if scenario.source.last.location.line < outline.location.line
124
- return false
125
- end
126
-
127
- # Now the last feature element needs to be an outline - this is a
128
- # sanity check that makes later stuff easier
129
- if not outline.respond_to? :examples_tables
130
- return false
131
- end
132
-
133
- # The last row of the last examples tables is what we care about
134
- last_row = outline.examples_tables.last.example_rows.last
135
-
136
- # If the current scenario has the same location as the last example,
137
- # then we're the last scenario.
138
- return scenario.source.last.location.line == last_row.location.line
139
-
140
- rescue
141
- # 1.3.x
142
- if scenario.scenario_outline.feature.feature_elements.last == scenario.scenario_outline
143
- # And is this the last example in the table?
144
- is_last_example = false
145
- scenario.scenario_outline.each_example_row do |row|
146
- if row == scenario
147
- is_last_example = true
148
- else
149
- # Overwrite this again with 'false'
150
- is_last_example = false
151
- end
152
- end
153
- return is_last_example
154
- end
155
- end
156
- end
157
- raise "If you see this error it might indicate you're running an unsupported version of cucumber"
158
- end
159
20
  end # module Ast
160
21
  end # module LapisLazuli
@@ -5,16 +5,13 @@
5
5
  # Copyright (c) 2013-2019 spriteCloud B.V. and other LapisLazuli contributors.
6
6
  # All rights reserved.
7
7
  #
8
-
9
8
  require "lapis_lazuli/ast"
10
-
11
9
  # Modules
12
10
  require "lapis_lazuli/browser/error"
13
11
  require 'lapis_lazuli/browser/find'
14
12
  require "lapis_lazuli/browser/wait"
15
13
  require "lapis_lazuli/browser/screenshots"
16
14
  require "lapis_lazuli/browser/interaction"
17
- require "lapis_lazuli/browser/remote"
18
15
  require 'lapis_lazuli/generic/xpath'
19
16
  require 'lapis_lazuli/generic/assertions'
20
17
 
@@ -27,19 +24,18 @@ module LapisLazuli
27
24
  # object, and for some WorldModules to exist in it (see assertions in
28
25
  # constructor).
29
26
  class Browser
30
- include LapisLazuli::Ast
31
27
 
32
28
  include LapisLazuli::BrowserModule::Error
33
29
  include LapisLazuli::BrowserModule::Find
34
30
  include LapisLazuli::BrowserModule::Wait
35
31
  include LapisLazuli::BrowserModule::Screenshots
36
32
  include LapisLazuli::BrowserModule::Interaction
37
- include LapisLazuli::BrowserModule::Remote
38
33
  include LapisLazuli::GenericModule::XPath
39
34
 
40
- @@world=nil
41
- @@cached_browser_options={}
42
- @@browsers=[]
35
+ @@world = nil
36
+ @@cached_browser_options = {}
37
+ @@browsers = []
38
+
43
39
  class << self
44
40
  include LapisLazuli::GenericModule::Assertions
45
41
 
@@ -70,11 +66,9 @@ module LapisLazuli
70
66
  end
71
67
 
72
68
  @browser
73
- @browser_name
74
- @browser_wanted
75
- @optional_data
69
+ @browser_args
76
70
 
77
- attr_reader :browser_name, :browser_wanted, :optional_data
71
+ attr_reader :browser_args
78
72
 
79
73
  def initialize(*args)
80
74
  # The class only works with some modules loaded; they're loaded by the
@@ -94,8 +88,7 @@ module LapisLazuli
94
88
  # Support browser.dup to create a duplicate
95
89
  def initialize_copy(source)
96
90
  super
97
- @optional_data = @optional_data.dup
98
- @browser = create_driver(@browser_wanted, @optional_data)
91
+ @browser = create_driver(*@browser_args)
99
92
  # Add this browser to the list of all browsers
100
93
  LapisLazuli::Browser.add_browser(self)
101
94
  end
@@ -124,7 +117,7 @@ module LapisLazuli
124
117
  # Add this browser to the list of all browsers
125
118
  LapisLazuli::Browser.add_browser(self)
126
119
  # Making sure all browsers are gracefully closed when the exit event is triggered.
127
- at_exit {LapisLazuli::Browser::close_all 'exit event trigger'}
120
+ at_exit { LapisLazuli::Browser::close_all 'exit event trigger' }
128
121
  end
129
122
  end
130
123
 
@@ -138,7 +131,7 @@ module LapisLazuli
138
131
 
139
132
  ##
140
133
  # Closes the browser and updates LL so that it will open a new one if needed
141
- def close(reason = nil, remove_from_list=true)
134
+ def close(reason = nil, remove_from_list = true)
142
135
  if not @browser.nil?
143
136
  if not reason.nil?
144
137
  reason = " after #{reason}"
@@ -171,18 +164,39 @@ module LapisLazuli
171
164
  # Determine the config
172
165
  close_browser_after = world.env_or_config("close_browser_after")
173
166
  case close_browser_after
174
- when "scenario"
175
- # We always close it
167
+ when "scenario"
168
+ # We always close it
169
+ LapisLazuli::Browser.close_all close_browser_after
170
+ when "never"
171
+ # Do nothing: party time, excellent!
172
+ when "feature"
173
+ if is_last_scenario?(scenario)
174
+ # Close it
176
175
  LapisLazuli::Browser.close_all close_browser_after
177
- when "never"
178
- # Do nothing: party time, excellent!
179
- when "feature"
180
- if is_last_scenario?(scenario)
181
- # Close it
182
- LapisLazuli::Browser.close_all close_browser_after
183
- end
184
- else # close after 'end' is now default
185
- # Also ignored here - this is handled in World.browser_destroy
176
+ end
177
+ else
178
+ # close after 'end' is now default
179
+ # Also ignored here - this is handled in World.browser_destroy
180
+ end
181
+ end
182
+
183
+ def is_last_scenario?(scenario)
184
+ begin
185
+ feature_file = File.read(scenario.location.file)
186
+ gherkin_object = Gherkin::Parser.new.parse(feature_file)
187
+ last_line = gherkin_object[:feature][:children].last[:scenario][:examples].last[:table_body].last[:location][:line] rescue nil
188
+ unless last_line
189
+ last_line = gherkin_object[:feature][:children].last[:scenario][:location][:line] rescue nil
190
+ end
191
+ if last_line
192
+ return last_line == scenario.location.line
193
+ else
194
+ warn 'Failed to find the last line of the feature trying to determine if this is the last scenario running.'
195
+ return false
196
+ end
197
+ rescue Exception => e
198
+ warn 'Something went wrong trying to determine if this is the last sceanrio of the feature.'
199
+ warn e
186
200
  end
187
201
  end
188
202
 
@@ -209,7 +223,7 @@ module LapisLazuli
209
223
  LapisLazuli::Browser.close_all("end")
210
224
  end
211
225
 
212
- def self.close_all(reason=nil)
226
+ def self.close_all(reason = nil)
213
227
  # A running browser should exist and we are allowed to close it
214
228
  if @@browsers.length != 0 and @@world.env_or_config("close_browser_after") != "never"
215
229
  # Notify user
@@ -225,68 +239,22 @@ module LapisLazuli
225
239
  end
226
240
 
227
241
  private
242
+
228
243
  ##
229
244
  # The main browser window for testing
230
- def init(browser_wanted=nil, optional_data=nil)
245
+ def init(*args)
231
246
  # Store the optional data so on restart of the browser it still has the correct configuration
232
- if optional_data.nil? and @@cached_browser_options.has_key?(:optional_data) and (browser_wanted.nil? or browser_wanted == @@cached_browser_options[:browser])
233
- optional_data = @@cached_browser_options[:optional_data].dup
234
- if !@@cached_browser_options[:optional_data][:profile].nil?
235
- # A selenium profile needs to be duplicated separately, else it doesn't get a new ID.
236
- optional_data[:profile] = @@cached_browser_options[:optional_data][:profile].dup
237
- end
238
- elsif optional_data.nil?
239
- optional_data = {}
240
- end
241
-
242
- # Do the same caching stuff for the browser
243
- if browser_wanted.nil? and @@cached_browser_options.has_key?(:browser)
244
- browser_wanted = @@cached_browser_options[:browser]
245
- end
246
-
247
- # Set the default device if the optional data does not contain a specific device
248
- if optional_data[:device].nil?
249
- # Check if there is a cached value of a previously used
250
- if @@cached_browser_options.has_key?(:device)
251
- optional_data[:device] = @@cached_browser_options[:device]
252
- # Check if the ENV['DEVICE'] variable is set
253
- elsif world.env_or_config('DEVICE', false)
254
- optional_data[:device] = world.env_or_config('DEVICE')
255
- # Else grab the default set device
256
- elsif world.env_or_config('default_device', false)
257
- optional_data[:device] = world.env_or_config('default_device')
258
- else
259
- warn 'No default device, nor a selected device was set. Browser default settings will be loaded. More info: http://testautomation.info/Lapis_Lazuli:Device_Simulation'
260
- end
261
- end
262
-
263
- # cache all the settings if this is the first time opening the browser.
264
- if !@@cached_browser_options.has_key? :browser and !@@cached_browser_options.has_key? :optional_data
265
- @@cached_browser_options[:browser] = browser_wanted
266
- # Duplicate the data as Webdriver modifies it
267
- @@cached_browser_options[:optional_data] = optional_data.dup
268
- if !@@cached_browser_options[:optional_data][:profile].nil?
269
- # A selenium profile needs to be duplicated separately, else it doesn't get a new ID.
270
- @@cached_browser_options[:optional_data][:profile] = optional_data[:profile].dup
271
- end
272
- end
273
-
274
- @browser_wanted = browser_wanted
275
- @optional_data = optional_data
276
- # Create the browser
277
- create_driver(@browser_wanted, @optional_data)
247
+ create_driver(*args)
278
248
  end
279
249
 
280
250
  ##
281
251
  # Create a new browser depending on settings
282
252
  # Always cached the supplied arguments
283
- def create_driver(browser_wanted=nil, optional_data=nil)
253
+ def create_driver(*args)
284
254
  # Remove device information from optional_data and create a separate variable for it
285
- device = optional_data[:device]
286
- optional_data.delete :device
287
-
255
+ device = args[1] ? args[1].delete(:device) : nil
288
256
  # If device is set, load it from the devices.yml config
289
- if !device.nil?
257
+ unless device.nil?
290
258
  begin
291
259
  world.add_config_from_file('./config/devices.yml')
292
260
  rescue
@@ -307,84 +275,6 @@ module LapisLazuli
307
275
  raise LoadError, "#{err}: you need to add 'watir' to your Gemfile before using the browser."
308
276
  end
309
277
 
310
- # No browser? Does the config have a browser?
311
- if browser_wanted.nil?
312
- browser_wanted = world.env_or_config('browser', nil)
313
- end
314
-
315
- b = browser_wanted
316
- b = b.to_sym unless b.nil?
317
-
318
- # Overwrite user-agent if a device simulation is set and it contains a user-agent
319
- if !device_configuration.nil? and !device_configuration['user-agent'].nil?
320
- # Firefox user-agent settings
321
- # Create a firefox profile if it does not exist yet
322
- if optional_data[:profile].nil?
323
- optional_data[:profile] = Selenium::WebDriver::Firefox::Profile.new
324
- else
325
- # If the profile already exists, we need to create a duplicate, so we don't overwrite any settings.
326
- optional_data[:profile] = optional_data[:profile].dup
327
- end
328
- # Add the user agent to it if it has not been set yet
329
- if optional_data[:profile].instance_variable_get(:@additional_prefs)['general.useragent.override'].nil?
330
- optional_data[:profile]['general.useragent.override'] = device_configuration['user-agent']
331
- else
332
- world.log.debug "User-agent was already set in the :profile."
333
- end
334
- # Chrome user-agent settings
335
- ua_string = "--user-agent=#{device_configuration['user-agent']}"
336
- if optional_data[:switches].nil?
337
- optional_data[:switches] = [ua_string]
338
- elsif !optional_data[:switches].join(',').include? '--user-agent='
339
- optional_data[:switches].push ua_string
340
- else
341
- world.log.debug "User-agent was already set in the :switches."
342
- end
343
- if b != :firefox and b != :chrome
344
- warn "#{device} user agent cannot be set for #{b.to_s}. Only Chrome & Firefox are supported."
345
- end
346
- end
347
-
348
- args = []
349
- args = [b] unless b.nil?
350
- @browser_name = b.to_s
351
- if b == :remote
352
- # Get the config
353
- remote_config = world.env_or_config("remote", {})
354
-
355
- # The settings we are going to use to create the browser
356
- remote_settings = {}
357
-
358
- # Add the config to the settings using downcase string keys
359
- remote_config.each {|k, v| remote_settings[k.to_s.downcase] = v}
360
-
361
- if optional_data.is_a? Hash
362
- # Convert the optional data to downcase string keys
363
- string_hash = Hash.new
364
- optional_data.each {|k, v| string_hash[k.to_s.downcase] = v}
365
-
366
- # Merge them with the settings
367
- remote_settings.merge! string_hash
368
- end
369
-
370
- args.push(remote_browser_config(remote_settings))
371
- elsif not optional_data.nil? and not optional_data.empty?
372
- world.log.debug("Got optional data: #{optional_data}")
373
- args.push(optional_data)
374
- elsif world.has_proxy?
375
- # Create a session if needed
376
- if !world.proxy.has_session?
377
- world.proxy.create()
378
- end
379
-
380
- proxy_url = "#{world.proxy.ip}:#{world.proxy.port}"
381
- if b == :firefox
382
- world.log.debug("Configuring Firefox proxy: #{proxy_url}")
383
- profile = Selenium::WebDriver::Firefox::Profile.new
384
- profile.proxy = Selenium::WebDriver::Proxy.new :http => proxy_url, :ssl => proxy_url
385
- args.push({:profile => profile})
386
- end
387
- end
388
278
  begin
389
279
  browser_instance = Watir::Browser.new(*args)
390
280
  # Resize the browser if the device simulation requires it
@@ -394,7 +284,7 @@ module LapisLazuli
394
284
  rescue Selenium::WebDriver::Error::UnknownError => err
395
285
  raise err
396
286
  end
397
- return browser_instance
287
+ browser_instance
398
288
  end
399
289
  end
400
290