locatine 0.02637 → 0.02651

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +268 -297
  3. data/bin/locatine-daemon.rb +4 -2
  4. data/lib/locatine/daemon.rb +44 -60
  5. data/lib/locatine/daemon_helpers/methods.rb +85 -0
  6. data/lib/locatine/element.rb +28 -0
  7. data/lib/locatine/results.rb +125 -0
  8. data/lib/locatine/results_helpers/common.rb +68 -0
  9. data/lib/locatine/results_helpers/find_by_magic.rb +106 -0
  10. data/lib/locatine/results_helpers/guess.rb +45 -0
  11. data/lib/locatine/results_helpers/info_generator.rb +79 -0
  12. data/lib/locatine/results_helpers/logger.rb +64 -0
  13. data/lib/locatine/{for_search → results_helpers}/xpath_generator.rb +18 -19
  14. data/lib/locatine/scripts/element.js +40 -0
  15. data/lib/locatine/scripts/page.js +53 -0
  16. data/lib/locatine/scripts/parent.js +6 -0
  17. data/lib/locatine/session.rb +102 -0
  18. data/lib/locatine/version.rb +4 -2
  19. data/lib/locatine.rb +3 -2
  20. metadata +39 -49
  21. data/lib/locatine/app/background.js +0 -8
  22. data/lib/locatine/app/content.css +0 -38
  23. data/lib/locatine/app/content.js +0 -152
  24. data/lib/locatine/app/devtools.html +0 -1
  25. data/lib/locatine/app/devtools.js +0 -3
  26. data/lib/locatine/app/manifest.json +0 -20
  27. data/lib/locatine/app/popup.css +0 -47
  28. data/lib/locatine/app/popup.html +0 -19
  29. data/lib/locatine/app/popup.js +0 -65
  30. data/lib/locatine/daemon_helpers.rb +0 -52
  31. data/lib/locatine/for_search/data_generate.rb +0 -67
  32. data/lib/locatine/for_search/data_logic.rb +0 -98
  33. data/lib/locatine/for_search/defaults.rb +0 -40
  34. data/lib/locatine/for_search/dialog_logic.rb +0 -107
  35. data/lib/locatine/for_search/element_selection.rb +0 -80
  36. data/lib/locatine/for_search/file_work.rb +0 -67
  37. data/lib/locatine/for_search/find_by_guess.rb +0 -67
  38. data/lib/locatine/for_search/find_by_locator.rb +0 -59
  39. data/lib/locatine/for_search/find_by_magic.rb +0 -65
  40. data/lib/locatine/for_search/find_logic.rb +0 -79
  41. data/lib/locatine/for_search/helpers.rb +0 -106
  42. data/lib/locatine/for_search/highlight.rb +0 -41
  43. data/lib/locatine/for_search/listening.rb +0 -48
  44. data/lib/locatine/for_search/merge.rb +0 -40
  45. data/lib/locatine/for_search/name_helper.rb +0 -51
  46. data/lib/locatine/for_search/page_work.rb +0 -126
  47. data/lib/locatine/for_search/public.rb +0 -179
  48. data/lib/locatine/for_search/saying.rb +0 -199
  49. data/lib/locatine/for_search.rb +0 -6
  50. data/lib/locatine/large_scripts/css.js +0 -21
  51. data/lib/locatine/large_scripts/dimensions.js +0 -17
  52. data/lib/locatine/large_scripts/element.js +0 -30
  53. data/lib/locatine/large_scripts/page.js +0 -60
  54. data/lib/locatine/scope.rb +0 -88
  55. data/lib/locatine/search.rb +0 -67
@@ -1,7 +1,9 @@
1
1
  #!Locatine-daemon...
2
+ # frozen_string_literal: true
3
+
2
4
  require 'locatine'
3
- args = Hash[ ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/) ]
4
- args = args.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
5
+ args = Hash[ARGV.join(' ').scan(/--?([^=\s]+)(?:=(\S+))?/)]
6
+ args = args.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = v; }
5
7
  args.each_pair do |key, value|
6
8
  Locatine::Daemon.set key, value
7
9
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sinatra/base'
2
4
  require 'json'
3
- require 'locatine/daemon_helpers'
5
+ require 'locatine/session'
6
+ require 'locatine/daemon_helpers/methods'
4
7
 
5
8
  module Locatine
6
9
  #
@@ -8,86 +11,67 @@ module Locatine
8
11
  #
9
12
  # run Locatine::Daemon.run!
10
13
  class Daemon < Sinatra::Base
11
- include Locatine::DaemonHelpers
12
- configure do
13
- set :search, nil
14
- end
14
+ include Locatine::DaemonHelpers::Methods
15
15
 
16
- get '/app' do
17
- { app: File.join(Locatine::HOME, 'app').to_s }.to_json
16
+ configure do
17
+ set :sessions, {}
18
+ set :selenium, ENV['SELENIUM'] || 'http://localhost:4444'
19
+ set :headers, 'Content-Type' => 'application/json'
20
+ set :port, 7733
18
21
  end
19
22
 
23
+ # own calls
20
24
  get '/' do
21
- redirect 'https://github.com/sseleznevqa/locatine#using-as-a-daemon'
25
+ redirect 'https://github.com/sseleznevqa/locatine'
22
26
  end
23
27
 
24
- get '/stop' do
28
+ get '/locatine/stop' do
25
29
  Locatine::Daemon.quit!
26
30
  { result: 'dead' }.to_json
27
31
  end
28
32
 
29
- post '/chromedriver' do
30
- Webdrivers::Chromedriver.required_version = params['version']
31
- { version: Webdrivers::Chromedriver.required_version }.to_json
32
- end
33
-
34
- get '/chromedriver' do
35
- { path: Webdrivers::Chromedriver.update }.to_json
36
- end
37
-
38
- post '/geckodriver' do
39
- Webdrivers::Geckodriver.required_version = params['version']
40
- { version: Webdrivers::Geckodriver.required_version }.to_json
33
+ post '/locatine/session/*' do
34
+ session = request.path_info.split('/').last
35
+ result = settings.sessions[session].configure(params)
36
+ { result: result }.to_json
41
37
  end
42
38
 
43
- get '/geckodriver' do
44
- { path: Webdrivers::Geckodriver.update }.to_json
39
+ # selenium calls
40
+ post '/wd/hub/session/*/element' do
41
+ content_type settings.headers['Content-Type']
42
+ results = settings.sessions[session_id].find(params, element_id)
43
+ status 200
44
+ results.empty? ? raise_not_found : { value: results.first.answer }.to_json
45
45
  end
46
46
 
47
- post 'iedriver' do
48
- Webdrivers::IEdriver.required_version = params['version']
49
- { version: Webdrivers::IEdriver.required_version }.to_json
47
+ post '/wd/hub/session/*/elements' do
48
+ content_type settings.headers['Content-Type']
49
+ results = settings.sessions[session_id].find(params, element_id)
50
+ status 200
51
+ answer = results.empty? ? [] : results.map(&:answer)
52
+ { value: answer }.to_json
50
53
  end
51
54
 
52
- get '/iedriver' do
53
- { path: Webdrivers::IEdriver.update }.to_json
55
+ post '/wd/hub/session' do
56
+ result = call_process('post')
57
+ the_session = JSON.parse(result)['value']['sessionId']
58
+ caps = params['desiredCapabilities']
59
+ locatine_caps = caps['locatine'] if caps
60
+ settings.sessions[the_session] = Locatine::Session
61
+ .new(selenium, the_session)
62
+ settings.sessions[the_session].configure(locatine_caps)
63
+ result
54
64
  end
55
65
 
56
- post '/connect' do
57
- steal
58
- { result: true }.to_json
66
+ delete '/wd/hub/session/*' do
67
+ settings.sessions[session_id] = nil
68
+ call_process('delete')
59
69
  end
60
70
 
61
- post '/lctr' do
62
- data = Hash[params.map { |k, v| [k.to_sym, v] }]
63
- data.each { |k, v| data[k] = false if v == 'false' }
64
- search.lctr(data).to_json
65
- end
66
-
67
- post '/set' do
68
- hash = params
69
- search.json = hash['json'] if hash['json']
70
- warn 'You cannot set browser like this. Use /connect' if hash['browser']
71
- params.each_pair do |key, value|
72
- unless (key == 'browser') || (key == 'json')
73
- value = false if value == 'false'
74
- search.instance_variable_set("@#{key}", value)
75
- end
71
+ %w[get post put patch delete].each do |verb|
72
+ send(verb, '/wd/hub/*') do
73
+ call_process(verb)
76
74
  end
77
- { result: true }.to_json
78
- end
79
-
80
- def search
81
- return settings.search unless settings.search.nil?
82
-
83
- settings.search = Locatine::Search.new
84
- settings.search.browser.quit
85
- settings.search
86
- end
87
-
88
- def params
89
- request.body.rewind
90
- JSON.parse request.body.read
91
75
  end
92
76
  end
93
77
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module Locatine
6
+ module DaemonHelpers
7
+ #
8
+ # Methods that are used by daemon
9
+ module Methods
10
+ def api_request(type, path, query_string, body, new_headers)
11
+ uri = make_uri(path, query_string)
12
+ req = Net::HTTP.const_get(type).new(uri,
13
+ settings.headers.merge(new_headers))
14
+ req.body = body.read
15
+ Net::HTTP.new(uri.hostname, uri.port).start { |http| http.request(req) }
16
+ end
17
+
18
+ def make_uri(path, query_string)
19
+ parsed = URI.parse selenium
20
+ URI::HTTP.build(
21
+ host: parsed.host,
22
+ port: parsed.port,
23
+ path: path,
24
+ query: query_string
25
+ )
26
+ end
27
+
28
+ def all_headers(response)
29
+ header_list = {}
30
+ response.header.each_capitalized do |k, v|
31
+ header_list[k] = v unless k == 'Transfer-Encoding'
32
+ end
33
+ header_list
34
+ end
35
+
36
+ def incomming_headers(request)
37
+ request.env.map do |header, value|
38
+ if header.start_with?('HTTP_')
39
+ [header[5..-1].split('_').map(&:capitalize).join('-'), value]
40
+ end
41
+ end .compact.to_h
42
+ end
43
+
44
+ def send_answer(response)
45
+ content_type settings.headers['Content-Type']
46
+ status response.code
47
+ headers all_headers(response)
48
+ response.body
49
+ end
50
+
51
+ def call_process(verb)
52
+ start_request = Thread.new do
53
+ api_request(verb.capitalize, request.path_info, request.query_string,
54
+ request.body, incomming_headers(request))
55
+ end
56
+ send_answer(start_request.value)
57
+ end
58
+
59
+ def params
60
+ request.body.rewind
61
+ JSON.parse request.body.read
62
+ end
63
+
64
+ def selenium
65
+ settings.selenium
66
+ end
67
+
68
+ def raise_not_found
69
+ status 404
70
+ { value: { error: 'no such element',
71
+ message: 'no such element: Unable to locate element',
72
+ stacktrace: 'Locatine has no idea too' } }.to_json
73
+ end
74
+
75
+ def session_id
76
+ request.path_info.split('/')[4]
77
+ end
78
+
79
+ def element_id
80
+ path_array = request.path_info.split('/')
81
+ path_array.size >= 7 ? path_array[6] : nil
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ #
5
+ # Locatine single element
6
+ class Element
7
+ attr_accessor :answer
8
+
9
+ def initialize(session, element_code)
10
+ @session = session
11
+ @answer = element_code
12
+ end
13
+
14
+ def parent
15
+ parent = File.read("#{HOME}/scripts/parent.js")
16
+ new_answer = @session.execute_script(parent, self)
17
+ new_answer.nil? ? nil : Locatine::Element.new(@session, new_answer)
18
+ end
19
+
20
+ def info
21
+ return @info if @info
22
+
23
+ info = File.read("#{HOME}/scripts/element.js")
24
+ @info = @session.execute_script(info, self)
25
+ @info
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'locatine/results_helpers/info_generator'
4
+ require 'locatine/results_helpers/xpath_generator'
5
+ require 'locatine/results_helpers/find_by_magic'
6
+ require 'locatine/results_helpers/common'
7
+ require 'locatine/results_helpers/logger'
8
+ require 'locatine/results_helpers/guess'
9
+
10
+ module Locatine
11
+ #
12
+ # Locatine results container
13
+ class Results < Array
14
+ include Locatine::ResultsHelpers::InfoGenerator
15
+ include Locatine::ResultsHelpers::XpathGenerator
16
+ include Locatine::ResultsHelpers::FindByMagic
17
+ include Locatine::ResultsHelpers::Common
18
+ include Locatine::ResultsHelpers::Logger
19
+ include Locatine::ResultsHelpers::Guess
20
+
21
+ attr_accessor :name, :config, :locator
22
+
23
+ def configure(session, locator, parent)
24
+ @session = session
25
+ @locator = locator.clone
26
+ read_locator
27
+ @parent = parent
28
+ end
29
+
30
+ def simple_find
31
+ path = @parent ? "/element/#{@parent}/elements" : '/elements'
32
+ selenium_found = @session.api_request(path, 'Post', @locator.to_json).body
33
+ JSON.parse(selenium_found)['value'].each do |item|
34
+ push Locatine::Element.new(@session, item)
35
+ end
36
+ self
37
+ # {"value"=>
38
+ # [{"element-6066-11e4-a52e-4f735466cecf"=>"c95a0580-4ac7-4c6d-..."},
39
+ # {"element-6066-11e4-a52e-4f735466cecf"=>"f419f6cf-1a04-4bc8-b246-..."},
40
+ # {"element-6066-11e4
41
+ end
42
+
43
+ def classic_find
44
+ first_attempt
45
+ locating = @locator['value'].empty? || tolerance.positive?
46
+ return unless locating
47
+
48
+ second_attempt
49
+ third_attempt if known
50
+ forth_attempt if known
51
+ end
52
+
53
+ def first_attempt
54
+ log_start
55
+ simple_find unless @locator['value'].empty?
56
+ warn_locator if !@locator['value'].empty? && empty?
57
+ end
58
+
59
+ def third_attempt
60
+ base = {}
61
+ base['0'] = known['0']
62
+ find_by_data(base) if empty?
63
+ end
64
+
65
+ def second_attempt
66
+ find_by_data if known && empty?
67
+ end
68
+
69
+ def forth_attempt
70
+ base = {}
71
+ base['0'] = known['0'].select { |item| trusted.include?(item['name']) }
72
+ find_by_data(base) if empty? && !trusted.empty? && !base['0'].empty?
73
+ end
74
+
75
+ def find
76
+ timer
77
+ classic_find
78
+ guess if name_only?
79
+ return self unless empty?
80
+
81
+ find_by_magic if known && tolerance.positive?
82
+ similar? ? found : not_found
83
+ end
84
+
85
+ def found
86
+ log_found
87
+ uniq
88
+ end
89
+
90
+ def not_found
91
+ warn_lost
92
+ []
93
+ end
94
+
95
+ def read_locator
96
+ case @locator['using']
97
+ when 'css selector'
98
+ # "button/*{json}*/"
99
+ read_locator_routine(%r{/\*(.*)\*/$})
100
+ when 'xpath'
101
+ # "//button['{json}']"
102
+ read_locator_routine(/\[\'(.*)\'\]$/)
103
+ when 'locatine'
104
+ read_locator_routine(/(.*)/)
105
+ end
106
+ end
107
+
108
+ def read_locator_routine(regexp)
109
+ matched = @locator['value'].match(regexp)
110
+ @config = matched ? config_provided(matched[1]) : {}
111
+ @locator['value'] = @locator['value'].gsub(matched[0], '') if matched
112
+ @locator = @config['locator'] if @config['locator']
113
+ @name = @config['name'] || @locator['value']
114
+ end
115
+
116
+ def config_provided(config)
117
+ JSON.parse(config)
118
+ rescue StandardError
119
+ result = {}
120
+ result['tolerance'] = 0 if config.start_with?('exactly')
121
+ result['name'] = config.gsub('exactly ', '')
122
+ result
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Some common methods without much logic
7
+ module Common
8
+ def timer
9
+ @time ||= Time.now
10
+ timeout > Time.now - @time
11
+ end
12
+
13
+ def name_only?
14
+ @locator['value'].empty? && !known && @name
15
+ end
16
+
17
+ def known
18
+ @session.elements[@name]
19
+ end
20
+
21
+ def unknown_no_text(item, hash)
22
+ ((item['type'] == '*') && (hash['type'] != 'text')) ||
23
+ ((item['type'] != 'text') && (hash['type'] == '*'))
24
+ end
25
+
26
+ def same_name_type(item, hash)
27
+ (item['name'] == hash['name']) && (item['type'] == hash['type'])
28
+ end
29
+
30
+ def info_hash_eq(item, hash)
31
+ # Return true
32
+ # If type is unknown (but not a text)
33
+ # Or when type and name are similar
34
+ (unknown_no_text(item, hash) || same_name_type(item, hash)) &&
35
+ # And at the same time values are (almost) the same
36
+ (item['value'].downcase == hash['value'].downcase)
37
+ end
38
+
39
+ def stability
40
+ @config['stability'] || @session.stability
41
+ end
42
+
43
+ def trusted
44
+ @config['trusted'] || @session.trusted
45
+ end
46
+
47
+ def untrusted
48
+ @config['untrusted'] || @session.untrusted
49
+ end
50
+
51
+ def tolerance
52
+ @config['tolerance'] || @session.tolerance
53
+ end
54
+
55
+ def timeout
56
+ @config['timeout'] || @session.timeout
57
+ end
58
+
59
+ def the_depth
60
+ @config['depth'] || @session.depth
61
+ end
62
+
63
+ def thread_out
64
+ Thread.current['out']
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Methods for active looking for element
7
+ module FindByMagic
8
+ def magic_thread
9
+ Thread.new do
10
+ while empty?
11
+ magic_find
12
+ clear unless similar?
13
+ end
14
+ end
15
+ end
16
+
17
+ def classic_thread
18
+ Thread.new do
19
+ while empty?
20
+ sleep 0.5
21
+ classic_find
22
+ end
23
+ end
24
+ end
25
+
26
+ def find_by_magic
27
+ warn_magic
28
+ classic = classic_thread
29
+ magic = magic_thread
30
+ sleep 0.1 while timer && !similar?
31
+ classic.kill
32
+ magic.kill
33
+ normalize_indexes(magic['out']) if empty? && (magic['out'].to_h != {})
34
+ self
35
+ end
36
+
37
+ def magic_routine(data)
38
+ Thread.current['out'] = {}
39
+ page = @session.page
40
+ data.each_pair do |depth, array|
41
+ get_trusted(array).each do |hash|
42
+ @temp = []
43
+ catch(page, hash, depth)
44
+ temp_results_push
45
+ end
46
+ end
47
+ end
48
+
49
+ def temp_results_push
50
+ @temp.uniq.each do |index|
51
+ thread_out[index] ||= 0
52
+ thread_out[index] += 1
53
+ end
54
+ end
55
+
56
+ def magic_find(data = known)
57
+ magic_routine(data)
58
+ normalize_indexes
59
+ end
60
+
61
+ def normalize_indexes(indexes = Thread.current['out'])
62
+ all = { 'using' => 'tag name', 'value' => '*' }
63
+ list = @session.api_request('/elements', 'Post', all.to_json).body
64
+ list = JSON.parse(list)['value']
65
+ answers = max_indexes(indexes).map { |index| list[index.to_i] }
66
+ answers.each do |item|
67
+ push Locatine::Element.new(@session, item)
68
+ end
69
+ end
70
+
71
+ def max_indexes(indexes)
72
+ max = 0
73
+ array = []
74
+ indexes.each_pair do |index, count|
75
+ next if count < max
76
+
77
+ array.push(index)
78
+ next if count == max
79
+
80
+ array = [index]
81
+ max = count
82
+ end
83
+ array
84
+ end
85
+
86
+ def catch(page, hash, depth)
87
+ page.each do |element|
88
+ caught = (element['data'].select do |item|
89
+ info_hash_eq(item, hash)
90
+ end).first
91
+ kids([element], depth) if caught
92
+ catch(element['children'], hash, depth)
93
+ end
94
+ end
95
+
96
+ def kids(array, depth)
97
+ array.each do |element|
98
+ @temp.push element['index']
99
+ if !element['children'].empty? && depth.to_i.positive?
100
+ kids(element['children'], depth)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Trying to guess element here
7
+ module Guess
8
+ def guess
9
+ warn_guess
10
+ @config['tolerance'] = 100
11
+ magic = Thread.new do
12
+ magic_find(guess_data)
13
+ end
14
+ sleep 0.1 while timer && !similar?
15
+ magic.kill
16
+ normalize_indexes(magic['out']) if empty? && (magic['out'].to_h != {})
17
+ end
18
+
19
+ def main_guess_data
20
+ answer = { '0' => [] }
21
+ parts = @name.split(/[\s\'\\]/)
22
+ parts.each do |item|
23
+ next if item.to_s.empty?
24
+
25
+ answer['0'].push('type' => '*',
26
+ 'name' => '*',
27
+ 'value' => item)
28
+ end
29
+ answer
30
+ end
31
+
32
+ def guess_data
33
+ answer = main_guess_data
34
+ # We expecting tag at the last position.
35
+ last = @name.split(/[\s\'\\]/).last.to_s
36
+ unless last.empty?
37
+ answer['0'].push('type' => 'tag',
38
+ 'name' => 'tag',
39
+ 'value' => last)
40
+ end
41
+ answer
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Methods for gathering element info
7
+ module InfoGenerator
8
+ def info
9
+ stability_bump(raw_info)
10
+ end
11
+
12
+ def raw_info
13
+ result = {}
14
+ i = 0
15
+ while i <= the_depth
16
+ all = parents(i)
17
+ data = all.map(&:info)
18
+ result[i.to_s] = common_info(data) unless data.empty?
19
+ i += 1
20
+ end
21
+ result
22
+ end
23
+
24
+ def parents(depth)
25
+ all = clone
26
+ depth.times do
27
+ all.map!(&:parent)
28
+ all.compact!
29
+ end
30
+ all
31
+ end
32
+
33
+ def catching_old_equal(hash, depth)
34
+ if known && known[depth]
35
+ old = known[depth].clone
36
+ catched = (old.select { |item| info_hash_eq(item, hash) }).first
37
+ end
38
+ catched ||= hash
39
+ catched
40
+ end
41
+
42
+ def hash_stability_bump(catched, max)
43
+ new_stability = catched['stability'].to_i + 1
44
+ max += 1 if max < stability
45
+ catched['stability'] = new_stability if new_stability <= stability
46
+ catched['stability'] = max if trusted.include?(catched['name'])
47
+ catched['stability'] = 0 if untrusted.include?(catched['name'])
48
+ catched
49
+ end
50
+
51
+ def stability_bump(data)
52
+ result = {}
53
+ data.each_pair do |depth, info_array|
54
+ result[depth] = []
55
+ max = max_stability(known.to_h[depth])
56
+ info_array.each do |hash|
57
+ catched = catching_old_equal(hash, depth)
58
+ result[depth].push hash_stability_bump(catched, max)
59
+ end
60
+ end
61
+ result
62
+ end
63
+
64
+ def common_info(elements_data)
65
+ merged = elements_data.first
66
+ elements_data.each do |element_data|
67
+ merged = info_sum(merged, element_data)
68
+ end
69
+ merged
70
+ end
71
+
72
+ def info_sum(one, two)
73
+ (one.map do |hash|
74
+ (two.select { |item| info_hash_eq(item, hash) }).first
75
+ end).compact
76
+ end
77
+ end
78
+ end
79
+ end