marta 0.26150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +299 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/example_project/p_object/test_page.rb +31 -0
  13. data/example_project/spec/p_object/pageobjects/MartaTestPage.json +1 -0
  14. data/example_project/spec/spec_helper.rb +11 -0
  15. data/example_project/spec/watir_test_page_spec.rb +12 -0
  16. data/example_project/tests_with_learning.sh +1 -0
  17. data/example_project/tests_without_learning.sh +1 -0
  18. data/lib/marta/black_magic.rb +147 -0
  19. data/lib/marta/classes_creation.rb +21 -0
  20. data/lib/marta/data/custom-xpath.html +22 -0
  21. data/lib/marta/data/custom-xpath.js +48 -0
  22. data/lib/marta/data/element-confirm.html +18 -0
  23. data/lib/marta/data/element-confirm.js +30 -0
  24. data/lib/marta/data/element.html +23 -0
  25. data/lib/marta/data/element.js +183 -0
  26. data/lib/marta/data/for_test.html +7 -0
  27. data/lib/marta/data/for_test.js +8 -0
  28. data/lib/marta/data/page.html +24 -0
  29. data/lib/marta/data/page.js +38 -0
  30. data/lib/marta/data/style.css +209 -0
  31. data/lib/marta/dialogs.rb +124 -0
  32. data/lib/marta/injector.rb +101 -0
  33. data/lib/marta/json_2_class.rb +145 -0
  34. data/lib/marta/lightning.rb +36 -0
  35. data/lib/marta/options_and_paths.rb +140 -0
  36. data/lib/marta/public_methods.rb +51 -0
  37. data/lib/marta/read_write.rb +44 -0
  38. data/lib/marta/simple_element_finder.rb +84 -0
  39. data/lib/marta/user_values_prework.rb +26 -0
  40. data/lib/marta/version.rb +4 -0
  41. data/lib/marta/x_path.rb +170 -0
  42. data/lib/marta.rb +62 -0
  43. data/marta.gemspec +28 -0
  44. metadata +156 -0
@@ -0,0 +1,124 @@
1
+ require 'marta/simple_element_finder'
2
+ require 'marta/x_path'
3
+ require 'marta/lightning'
4
+ require 'marta/injector'
5
+ require 'marta/public_methods'
6
+ module Marta
7
+
8
+ #
9
+ # All many-steps dialogs should be here
10
+ #
11
+ # There is at least one situation when getting info from user is not so simple
12
+ # We need dialogs for cases like that. Now there is only dialog about method
13
+ module Dialogs
14
+
15
+ private
16
+
17
+ #
18
+ # Dialog operator class
19
+ #
20
+ # @note It is believed that no user will use it
21
+ class MethodSpeaker
22
+
23
+ include XPath, Lightning, Injector, PublicMethods
24
+
25
+ def initialize(class_name, method_name, data, requestor)
26
+ @class_name = class_name
27
+ @method_name = method_name
28
+ @data = data
29
+ @title = class_name+ '.' + method_name.to_s
30
+ @requestor = requestor
31
+ end
32
+
33
+ # Standart question
34
+ def ask(what, title = 'Some title', data = Hash.new)
35
+ inject(what, title, data)
36
+ end
37
+
38
+ # Main method. All the dialog logic is here
39
+ def dialog
40
+ while !finished? do
41
+ @attrs = ask_for_elements
42
+ @mass = get_elements_by_attrs
43
+ @styles = mass_highlight_turn @mass
44
+ @result = ask_confirmation
45
+ mass_highlight_turn(@mass, false, @styles)
46
+ end
47
+ if @result == '1'
48
+ standart_meth_merge
49
+ else
50
+ xpath_meth_merge
51
+ end
52
+ end
53
+
54
+ # Asking: "What are you looking for?"
55
+ def ask_for_elements
56
+ ask 'element', @title, @data['meths'][@method_name]
57
+ end
58
+
59
+ # Creating data to save when it is a basically defined element
60
+ def standart_meth_merge
61
+ temp = temp_hash
62
+ temp['meths'][@method_name] = @attrs
63
+ @data['meths'].merge!(temp['meths'])
64
+ @data
65
+ end
66
+
67
+ # Creating data to save when user suggests a custom xpath
68
+ def xpath_meth_merge
69
+ temp = temp_hash
70
+ temp['meths'][@method_name]['options'] = @result
71
+ @data['meths'].merge!(temp['meths'])
72
+ @data
73
+ end
74
+
75
+ # Finding out what was selected
76
+ def get_elements_by_attrs
77
+ xpath = XPathFactory.new(@attrs, @requestor).generate_xpath
78
+ engine.elements(xpath: xpath)
79
+ end
80
+
81
+ # Asking: "Are you sure?"
82
+ def ask_confirmation
83
+ ask 'element-confirm', @title, @mass.length.to_s
84
+ end
85
+
86
+ # Asking: "Provide your xpath"
87
+ def ask_xpath
88
+ ask 'custom-xpath', @title
89
+ end
90
+
91
+ #
92
+ # Is dialog finished?
93
+ #
94
+ # JS returning '1' when it's done. That is not good
95
+ # and should be rewrited as soon as possible
96
+ def finished?
97
+ if @result == '1'
98
+ true
99
+ elsif @result == '3'
100
+ @result = ask_xpath
101
+ (@result != '2') ? true : false
102
+ else
103
+ false
104
+ end
105
+ end
106
+
107
+ # Forming of an empty hash for storing element info
108
+ def temp_hash
109
+ temp, temp['meths'], temp['meths'][@method_name],
110
+ temp['meths'][@method_name]['options'] = Hash.new, Hash.new, Hash.new,
111
+ Hash.new
112
+ temp
113
+ end
114
+ end
115
+
116
+ # Method definition process
117
+ def user_method_dialogs(my_class_name, method_name, data)
118
+ dialog_master = MethodSpeaker.new(my_class_name, method_name, data, self)
119
+ data = dialog_master.dialog
120
+ file_write(my_class_name.to_s, data)
121
+ data
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,101 @@
1
+ module Marta
2
+
3
+ #
4
+ # This module can add Marta's stuff to the page.
5
+ #
6
+ # Marta is inserting tons of stuff to the pages!
7
+ # Those insertions are the only way for her to perform dialog with user
8
+ module Injector
9
+
10
+ private
11
+
12
+ #
13
+ # We are injecting things to the page using the Syringe.
14
+ #
15
+ # @note It is believed that no user will use it
16
+ class Syringe
17
+
18
+ def initialize(engine, marta_what, title = 'Something important',
19
+ old_data = Hash.new, folder = gem_libdir)
20
+ @what = marta_what
21
+ @title = title
22
+ @data = old_data
23
+ @engine = engine
24
+ @folder = folder
25
+ end
26
+
27
+ # "first" or "last".
28
+ def get_where(first)
29
+ first ? "first" : "last"
30
+ end
31
+
32
+ # Inserting to the page
33
+ def insert_to_page(tag, inner, first = true)
34
+ where = get_where(first)
35
+ script = <<-JS
36
+ var newMartaObject = document.createElement('#{tag}');
37
+ newMartaObject.setAttribute('martaclass','marta_#{tag}');
38
+ newMartaObject.innerHTML = '#{inner}';
39
+ document.body.insertBefore(newMartaObject,document.body.#{where}Child);
40
+ JS
41
+ @engine.execute_script script.gsub("\n",'')
42
+ end
43
+
44
+ # Taking a correct js file to inject
45
+ def js
46
+ File.read(@folder + "/data/#{@what}.js").gsub("\n",'')
47
+ end
48
+
49
+ # Taking a correct html file to inject
50
+ def html
51
+ File.read(@folder + "/data/#{@what}.html").gsub("\n",'')
52
+ end
53
+
54
+ # Taking a correct css file to inject
55
+ def style
56
+ File.read(@folder + "/data/style.css").gsub("\n",'')
57
+ end
58
+
59
+ # Injecting everything to the page
60
+ def files_to_page
61
+ insert_to_page('div', html)
62
+ insert_to_page('script', js, false)
63
+ insert_to_page('style', style, false)
64
+ end
65
+
66
+ # It is never used without get_result.
67
+ # But it can be used to show some message for user
68
+ def actual_injection
69
+ files_to_page
70
+ @data ||= Hash.new
71
+ insert_to_page('script', "var marta_what = \"#{@title}\"", false)
72
+ insert_to_page('script', "var old_marta_Data = #{@data}".gsub('=>',':'),
73
+ false)
74
+ @engine.execute_script("marta_add_data();")
75
+ end
76
+
77
+ # Retrieving result if js var = marta_confirm_mark is true
78
+ # we are returning js var = marta_result. So custom js should always
79
+ # return both.
80
+ def get_result
81
+ result = false
82
+ while result != true
83
+ # When Marta can't get a result she is reinjecting her stuff
84
+ begin
85
+ result = @engine.execute_script("return marta_confirm_mark")
86
+ rescue
87
+ actual_injection
88
+ end
89
+ end
90
+ @engine.execute_script("return marta_result")
91
+ end
92
+ end
93
+
94
+ # That's how Marta is injecting and retrieving result
95
+ def inject(what, title = 'Something important', data = Hash.new)
96
+ syringe = Syringe.new(engine, what, title, data, gem_libdir)
97
+ syringe.actual_injection
98
+ syringe.get_result
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,145 @@
1
+ require 'marta/options_and_paths'
2
+ require 'marta/read_write'
3
+ module Marta
4
+
5
+ #
6
+ # Here Marta is reading json files and precreating pageobject classes
7
+ # which were defined previously
8
+ #
9
+ # Main trick of the Marta is parsing jsons files to classes.
10
+ # For example valid Foo.json file in a valid folder will turn into Foo class.
11
+ # Class content differs when Marta is set to learning mode and when it's not.
12
+ # Class will have methods = watir elements
13
+ # and vars = user defined vars with default values.
14
+ # Class will not accept any arguments for generated methods.
15
+ # The class will have default initialize method, engine method.
16
+ #
17
+ # Also the class can has method_edit method. In theory it can be called like
18
+ # Foo.method_edit('new_method_name').
19
+ # It should define new method even if learn mode is disabled.
20
+ # But I am never using such construction :)
21
+ # In learn mode any unknown method will cause dialog that will ask user about
22
+ # what element should be used.
23
+ #
24
+ # Also for each method foo method foo_exact will be created and vice versa.
25
+ # Method wich ends with exact will use strict element searching scheme.
26
+ module Json2Class
27
+
28
+ private
29
+
30
+ #
31
+ # To create a special class we are using a special class
32
+ #
33
+ # @note It is believed that no user will use it
34
+ class SmartPageCreator
35
+
36
+ include OptionsAndPaths, ReadWrite
37
+
38
+ #
39
+ # Main class creation method. SmartPage method is only working with
40
+ # a proper initialization.
41
+ #
42
+ # By default SmartPage has no initialization which is making it almost
43
+ # useles while not constructed like that. It will be fixed.
44
+ def self.create(class_name, data, edit)
45
+ c = Class.new(SmartPage) do
46
+ define_method :initialize do |my_data=data, my_class_name=class_name,
47
+ will_edit=edit|
48
+ @data = my_data
49
+ @class_name = class_name
50
+ @edit_mark = will_edit
51
+ build_content my_data
52
+ if will_edit
53
+ page_edit my_class_name, my_data
54
+ end
55
+ # We need optimization here very much!
56
+ build_content my_data
57
+ end
58
+ end
59
+ # We are vanishing previous version of class
60
+ if Kernel.constants.include?(class_name.to_sym)
61
+ Kernel.send(:remove_const, class_name.to_sym)
62
+ end
63
+ # We are declaring our class
64
+ Kernel.const_set class_name, c
65
+ end
66
+
67
+ # We are parsing file into a class
68
+ def self.json_2_class(json, edit_enabled = true)
69
+ data = ReaderWriter.file_2_hash(json)
70
+ if !data.nil?
71
+ class_name = File.basename(json, ".*")
72
+ edit_mark = SettingMaster.learn_status and edit_enabled
73
+ create class_name, data, edit_mark
74
+ end
75
+ end
76
+
77
+ # Marta is parsing all the files in pageobject folder into classes
78
+ def self.create_all
79
+ if File.directory?(SettingMaster.pageobjects_folder)
80
+ Dir["#{SettingMaster.pageobjects_folder}/*.json"].each do |file_name|
81
+ json_2_class(file_name, true) #true here
82
+ end
83
+ else
84
+ FileUtils::mkdir_p SettingMaster.pageobjects_folder
85
+ end
86
+ end
87
+ end
88
+
89
+ def read_folder
90
+ SmartPageCreator.create_all
91
+ end
92
+
93
+ def json_2_class(json, edit_enabled = true)
94
+ SmartPageCreator.json_2_class(json, edit_enabled)
95
+ end
96
+
97
+ def build_content(data)
98
+ build_methods(data['meths'])
99
+ build_vars(data['vars'])
100
+ end
101
+
102
+ def build_methods(methods)
103
+ methods.each_pair do |method_name, content|
104
+ build_method method_name, content
105
+ end
106
+ end
107
+
108
+ def build_vars(vars)
109
+ vars.each do |var_name, default_value|
110
+ build_var var_name, default_value
111
+ end
112
+ end
113
+
114
+ def build_method(name, content)
115
+ define_singleton_method name.to_sym do
116
+ learn_status ? method_edit(name) : marta_magic_finder(content)
117
+ end
118
+ exact = name + '_exact'
119
+ define_singleton_method exact.to_sym do
120
+ learn_status ? method_edit(exact) : marta_simple_finder(content)
121
+ end
122
+ end
123
+
124
+ def build_var(name, content)
125
+ if !self.methods.include?(name.to_sym) and (@data['meths'][name] == nil)
126
+ self.singleton_class.send(:attr_accessor, name.to_sym)
127
+ instance_variable_set("@#{name}", process_string(content))
128
+ else
129
+ if !@data['meths'][name].nil?
130
+ warn "Marta will not create '#{name}' variable for #{self.class}"\
131
+ " since it is already in use by method"
132
+ end
133
+ end
134
+ end
135
+
136
+ def correct_name(name)
137
+ if name.to_s.end_with? "_exact"
138
+ method_name = name.to_s[0..-7]
139
+ else
140
+ method_name = name.to_s
141
+ end
142
+ method_name
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,36 @@
1
+ module Marta
2
+
3
+ # Marta can highlight or unhighlight elements when her styles are injected.
4
+ module Lightning
5
+
6
+ private
7
+
8
+ # We can highlight an element
9
+ def highlight(element)
10
+ orig_style = element.attribute_value("style")
11
+ engine.execute_script("arguments[0].setAttribute(arguments[1],"\
12
+ " arguments[2])", element, "style",
13
+ "animation: marta_found 6s infinite;")
14
+ orig_style
15
+ end
16
+
17
+ # We can unhighlight an element
18
+ def unhighlight(element, style)
19
+ engine.execute_script("arguments[0].setAttribute(arguments[1],"\
20
+ " arguments[2])", element, "style", style)
21
+ end
22
+
23
+ # We can highlight\unhighlight tons of elements at once
24
+ def mass_highlight_turn(mass, turn_on = true, styles = nil)
25
+ result = Array.new
26
+ mass.each_with_index do |element, i|
27
+ if turn_on
28
+ result[i] = highlight(element)
29
+ else
30
+ unhighlight(element, styles[i])
31
+ end
32
+ end
33
+ result
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,140 @@
1
+ module Marta
2
+
3
+ #
4
+ # Marta can store and return settings which may differ for each thread
5
+ #
6
+ # Settings for Marta.
7
+ # Most of them could be changed in action using dance_with.
8
+ # Most of them are thread dependant.
9
+ module OptionsAndPaths
10
+
11
+ #
12
+ # We are storing vars in a special class
13
+ #
14
+ # @note It is believed that no user will use it
15
+ class SettingMaster
16
+ @@folder = Hash.new
17
+ @@tolerancy = Hash.new
18
+ @@learn = Hash.new
19
+ @@engine = Hash.new
20
+
21
+ # Getting uniq id for process thread
22
+ def self.thread_id
23
+ Thread.current.object_id
24
+ end
25
+
26
+ # Checking default learn option status
27
+ def self.learn_option
28
+ ENV['LEARN'].nil? ? false : true
29
+ end
30
+
31
+ # Marta knows does she learn or not.
32
+ def self.learn_status
33
+ if @@learn[thread_id].nil?
34
+ learn_option
35
+ else
36
+ @@learn[thread_id]
37
+ end
38
+ end
39
+
40
+ # Marta knows where are her saved generated pageobjects
41
+ def self.pageobjects_folder
42
+ @@folder[thread_id]
43
+ end
44
+
45
+ # Marta knows how hard she should search for elements
46
+ def self.tolerancy_value
47
+ @@tolerancy[thread_id]
48
+ end
49
+
50
+ # engine (analog of browser) is a setting too
51
+ def self.engine
52
+ @@engine[thread_id]
53
+ end
54
+
55
+ # Marta is changing parameters by the same scheme.
56
+ def self.parameter_set(what, value, default)
57
+ what[thread_id] = !value.nil? ? value : what[thread_id]
58
+ what[thread_id] = what[thread_id].nil? ? default : what[thread_id]
59
+ what
60
+ end
61
+
62
+ # Marta locates iframes sometimes
63
+ def self.iframe_locate
64
+ if !engine.nil?
65
+ if engine.class == Watir::IFrame
66
+ engine.locate
67
+ end
68
+ end
69
+ end
70
+
71
+ # Marta is switching to iframes sometimes
72
+ def self.iframe_switch_to
73
+ if !engine.nil?
74
+ if engine.class == Watir::IFrame
75
+ engine.switch_to!
76
+ end
77
+ end
78
+ end
79
+
80
+ # Marta is setting engine by pretty comlex rules
81
+ def self.set_engine(value)
82
+ iframe_locate
83
+ @@engine = parameter_set(@@engine, value, nil)
84
+ iframe_switch_to
85
+ if engine.nil?
86
+ @@engine = parameter_set(@@engine, value, Watir::Browser.new(:chrome))
87
+ end
88
+ end
89
+
90
+ # Marta uses simple rules to set the folder
91
+ def self.set_folder(value)
92
+ @@folder = parameter_set(@@folder, value, 'Marta_s_pageobjects')
93
+ end
94
+
95
+ # Marta uses simple rules to set the laearn mode
96
+ def self.set_learn(value)
97
+ @@learn = parameter_set(@@learn, value, learn_option)
98
+ end
99
+
100
+ # Marta uses simple rules to set the tolerancy value
101
+ def self.set_tolerancy(value)
102
+ @@tolerancy = parameter_set(@@tolerancy, value, 1024)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Defining the place for files to inject to browser
109
+ def gem_libdir
110
+ t = ["#{File.dirname(File.expand_path($0))}/../lib/#{Marta::NAME}",
111
+ "#{Gem.dir}/gems/#{Marta::NAME}-#{Marta::VERSION}/lib/#{Marta::NAME}"]
112
+ t.each {|i| return i if File.readable?(i) }
113
+ end
114
+
115
+ # Marta knows does she learn or not.
116
+ def learn_status
117
+ SettingMaster.learn_status
118
+ end
119
+
120
+ # Marta knows where are her saved generated pageobjects
121
+ def pageobjects_folder
122
+ SettingMaster.pageobjects_folder
123
+ end
124
+
125
+ # Marta knows how hard she should search for elements
126
+ def tolerancy_value
127
+ SettingMaster.tolerancy_value
128
+ end
129
+
130
+ # Marta is accepting parameters and rereading pageobjects on any change
131
+ def dance_with(browser: nil, folder: nil, learn: nil, tolerancy: nil)
132
+ SettingMaster.set_engine browser
133
+ SettingMaster.set_folder folder
134
+ SettingMaster.set_learn learn
135
+ read_folder
136
+ SettingMaster.set_tolerancy tolerancy
137
+ engine
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,51 @@
1
+ require 'marta/options_and_paths'
2
+ module Marta
3
+
4
+ # Methods that user can use out of the box in SmartPage
5
+ module PublicMethods
6
+
7
+ include OptionsAndPaths
8
+
9
+ # User can define a method for example in a middle of a debug session
10
+ def method_edit(name)
11
+ method_name = correct_name(name)
12
+ exact_name = method_name.to_s + "_exact"
13
+ data = user_method_dialogs(@class_name, method_name, @data)
14
+ define_singleton_method method_name.to_sym do |meth_content=@data['meths'][method_name]|
15
+ marta_magic_finder(meth_content)
16
+ end
17
+ define_singleton_method exact_name.to_sym do |meth_content=@data['meths'][method_name]|
18
+ marta_simple_finder(meth_content)
19
+ end
20
+ public_send name.to_sym
21
+ end
22
+
23
+ # User can get engine (normally browser instance or iframe element)
24
+ def engine
25
+ SettingMaster.engine
26
+ end
27
+
28
+ # If page has url variable it can be opened like Page.new.open_page
29
+ def open_page(url = nil)
30
+ if url != nil
31
+ engine.goto url
32
+ else
33
+ if @url == nil
34
+ raise ArgumentError, "You should set url to use open_page"
35
+ end
36
+ engine.goto @url
37
+ end
38
+ end
39
+
40
+ alias_method :default_method_missing, :method_missing
41
+
42
+ # method missing hijacking It should be used only for SmartPage
43
+ def method_missing(method_name, *args, &block)
44
+ if learn_status
45
+ method_edit(method_name)# , *args, &block)
46
+ else
47
+ default_method_missing(method_name, *args, &block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ require 'marta/options_and_paths'
2
+ module Marta
3
+
4
+ # Marta has very simple read\write actions
5
+ module ReadWrite
6
+
7
+ private
8
+
9
+ #
10
+ # Sometimes marta reads files. Sometimes writes
11
+ #
12
+ # @note It is believed that no user will use it
13
+ class ReaderWriter
14
+ include OptionsAndPaths
15
+ # Marta is writing to jsons from time to time
16
+ def self.file_write(name, data)
17
+ file_name = File.join(SettingMaster.pageobjects_folder, name + '.json')
18
+ File.open(file_name,"w") do |f|
19
+ f.write(data.to_json)
20
+ end
21
+ file_name
22
+ end
23
+
24
+ # Marta reads file to hash if it is a valid json
25
+ # If it is not a json file Marta will treat it like nothing
26
+ def self.file_2_hash(json)
27
+ begin
28
+ file = File.read(json)
29
+ data = JSON.parse(file)
30
+ rescue
31
+ nil
32
+ end
33
+ end
34
+ end
35
+
36
+ def file_write(name, data)
37
+ ReaderWriter.file_write(name, data)
38
+ end
39
+
40
+ def file_2_hash(json)
41
+ ReaderWriter.file_2_hash(json)
42
+ end
43
+ end
44
+ end