locatine 0.0092
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/locatine/app/background.js +9 -0
- data/lib/locatine/app/content.css +40 -0
- data/lib/locatine/app/content.js +94 -0
- data/lib/locatine/app/devtools.html +1 -0
- data/lib/locatine/app/devtools.js +3 -0
- data/lib/locatine/app/manifest.json +20 -0
- data/lib/locatine/app/popup.css +41 -0
- data/lib/locatine/app/popup.html +17 -0
- data/lib/locatine/app/popup.js +50 -0
- data/lib/locatine/search.rb +601 -0
- data/lib/locatine/version.rb +6 -0
- data/lib/locatine.rb +2 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 44a26e49cfff1bf0841fd588c9556fa0fbb1b394
|
4
|
+
data.tar.gz: 8f5b147ce47421e947be9ccdc2191da0fa746621
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3d151d506c89a6990640d34515519121a1b5b0da0c5d4797d3751b2b486ce7cb094ec90a7adab71c5580a2f2dbcd88c8f169c0c078f75c0c35bcc05589db9fbc
|
7
|
+
data.tar.gz: 3f6251ba3eed8756ca023d47a43b57953210463670430320cf46d06e762113796b2bc8f12d51f58d89f5d2c873e7de602787f5cfb4913d6d39f74d422755eea4
|
@@ -0,0 +1,40 @@
|
|
1
|
+
@keyframes locatine_found {
|
2
|
+
0% {border: 4px dashed #6495ED;}
|
3
|
+
20% {border: 4px dashed #FF7F50;}
|
4
|
+
40% {border: 4px dashed #8FBC8F;}
|
5
|
+
60% {border: 4px dashed #FFA500;}
|
6
|
+
80% {border: 4px dashed #DDA0DD;}
|
7
|
+
100% {border: 4px dashed #6495ED;}
|
8
|
+
}
|
9
|
+
|
10
|
+
div[locatinestyle=true] {
|
11
|
+
position:fixed;
|
12
|
+
padding:0;
|
13
|
+
margin:0;
|
14
|
+
top:0;
|
15
|
+
left:0;
|
16
|
+
opacity: 0;
|
17
|
+
background: black;
|
18
|
+
height: 100%;
|
19
|
+
width: 100%;
|
20
|
+
pointer-events: visible;
|
21
|
+
display: block;
|
22
|
+
z-index: 2147483646;
|
23
|
+
}
|
24
|
+
|
25
|
+
div[locatinestyle=false] {
|
26
|
+
height: 0%;
|
27
|
+
width: 0%;
|
28
|
+
}
|
29
|
+
|
30
|
+
div[locatinestyle=blocked] {
|
31
|
+
height: 0%;
|
32
|
+
width: 0%;
|
33
|
+
}
|
34
|
+
|
35
|
+
[locatineclass=foundbylocatine]
|
36
|
+
{animation: locatine_found 6s infinite;
|
37
|
+
-webkit-appearance: none;
|
38
|
+
-moz-appearance: none;
|
39
|
+
appearance: none;
|
40
|
+
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
async function set_value(name, value){
|
2
|
+
let temp = {};
|
3
|
+
temp[name] = value;
|
4
|
+
await chrome.storage.sync.set(temp, function() {});
|
5
|
+
};
|
6
|
+
|
7
|
+
async function get_value(name) {
|
8
|
+
let x = await new Promise((resolve, reject) => chrome.storage.sync.get([name], resolve));
|
9
|
+
return x[name];
|
10
|
+
};
|
11
|
+
|
12
|
+
document.addEventListener("locatine_send", async function(e) {
|
13
|
+
await set_value(e.detail.varname, e.detail.varvalue);
|
14
|
+
});
|
15
|
+
|
16
|
+
async function refreshData(){
|
17
|
+
if (!document.getElementById("locatine_magic_div")){
|
18
|
+
const options = {
|
19
|
+
"locatineclass": "locatine_smthing",
|
20
|
+
"id":"locatine_magic_div",
|
21
|
+
"locatinestyle": await get_value('magic_div') || "false",
|
22
|
+
"locatinetitle": "ok",
|
23
|
+
"locatinehint": "ok"
|
24
|
+
};
|
25
|
+
locatine_create_element(document.body, "div", options, "");
|
26
|
+
} else {
|
27
|
+
const magicDiv = document.getElementById("locatine_magic_div");
|
28
|
+
if (magicDiv.getAttribute("locatinestyle") === "set_true") {
|
29
|
+
await set_value('magic_div', true);
|
30
|
+
magicDiv.setAttribute("locatinestyle", "true");
|
31
|
+
};
|
32
|
+
if (magicDiv.getAttribute("locatinetitle") != "ok") {
|
33
|
+
await set_value('locatine_title', magicDiv.getAttribute("locatinetitle"));
|
34
|
+
await set_value('locatine_hint', magicDiv.getAttribute("locatinehint"));
|
35
|
+
magicDiv.setAttribute("locatinetitle", "ok");
|
36
|
+
}
|
37
|
+
let status = await get_value('magic_div');
|
38
|
+
if (magicDiv.getAttribute("locatinestyle") != "set_true"){
|
39
|
+
magicDiv.setAttribute("locatinestyle", status);
|
40
|
+
}
|
41
|
+
magicDiv.setAttribute("locatinecollection", await get_value("locatine_collection"))
|
42
|
+
if (magicDiv.getAttribute("locatineconfirmed") === "ok") {
|
43
|
+
magicDiv.removeAttribute("tag");
|
44
|
+
magicDiv.removeAttribute("index");
|
45
|
+
await set_value("locatine_confirm", false);
|
46
|
+
await set_value('magic_div', false);
|
47
|
+
}
|
48
|
+
const confirmed = await get_value('locatine_confirm');
|
49
|
+
magicDiv.setAttribute("locatineconfirmed", confirmed);
|
50
|
+
};
|
51
|
+
const magic_cover = document.getElementById('locatine_magic_div');
|
52
|
+
magic_cover.onclick = function(e) {locatine_magic_click(e)};
|
53
|
+
};
|
54
|
+
|
55
|
+
function getSelected(value){
|
56
|
+
const tagName = value.tagName;
|
57
|
+
const array = Array.prototype.slice.call( document.getElementsByTagName(tagName) );
|
58
|
+
const index = array.indexOf(value);
|
59
|
+
document.getElementById("locatine_magic_div").setAttribute("tag", tagName);
|
60
|
+
document.getElementById("locatine_magic_div").setAttribute("index", index);
|
61
|
+
};
|
62
|
+
|
63
|
+
function locatine_magic_click(e) {
|
64
|
+
document.getElementById("locatine_magic_div").setAttribute("locatinestyle", "blocked");
|
65
|
+
const value = document.elementFromPoint(e.clientX, e.clientY);
|
66
|
+
document.getElementById("locatine_magic_div").setAttribute("locatinestyle", "true");
|
67
|
+
const tagName = value.tagName;
|
68
|
+
const array = Array.prototype.slice.call( document.getElementsByTagName(tagName) );
|
69
|
+
const index = array.indexOf(value);
|
70
|
+
document.getElementById("locatine_magic_div").setAttribute("TAG", tagName);
|
71
|
+
document.getElementById("locatine_magic_div").setAttribute("INDEX", index);
|
72
|
+
};
|
73
|
+
|
74
|
+
function locatine_create_element(dom, tag, attrs, inner) {
|
75
|
+
const element = document.createElement(tag);
|
76
|
+
dom.appendChild(element);
|
77
|
+
for (var key in attrs) {
|
78
|
+
element.setAttribute(key, attrs[key])
|
79
|
+
};
|
80
|
+
element.innerHTML = inner;
|
81
|
+
return element;
|
82
|
+
};
|
83
|
+
|
84
|
+
//set_value('magic_div','off');
|
85
|
+
|
86
|
+
setInterval(async function(){
|
87
|
+
if (document.getElementById("locatine_magic_div")) {
|
88
|
+
if (document.getElementById("locatine_magic_div").getAttribute("locatinestyle") != "blocked") {
|
89
|
+
await refreshData()
|
90
|
+
}
|
91
|
+
} else {
|
92
|
+
await refreshData()
|
93
|
+
}
|
94
|
+
}, 100);
|
@@ -0,0 +1 @@
|
|
1
|
+
<script src="devtools.js"></script>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"name": "Locatine app",
|
3
|
+
"version": "0.0092",
|
4
|
+
"description": "Messaging from browser to main app",
|
5
|
+
"devtools_page": "devtools.html",
|
6
|
+
"permissions": ["activeTab", "storage", "contextMenus", "tabs"],
|
7
|
+
"background": {
|
8
|
+
"scripts": ["background.js"],
|
9
|
+
"persistent": true
|
10
|
+
},
|
11
|
+
"content_scripts": [{
|
12
|
+
"matches": ["<all_urls>"],
|
13
|
+
"all_frames": true,
|
14
|
+
"js": ["content.js"],
|
15
|
+
"css": ["content.css"]
|
16
|
+
}],
|
17
|
+
"browser_action": {
|
18
|
+
"default_title": "Locatine"},
|
19
|
+
"manifest_version": 2
|
20
|
+
}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
.button {
|
2
|
+
border: none;
|
3
|
+
height: 30px;
|
4
|
+
width: 100%;
|
5
|
+
color: white;
|
6
|
+
padding: 5px 32px;
|
7
|
+
text-align: center;
|
8
|
+
text-decoration: none;
|
9
|
+
display: inline-block;
|
10
|
+
font-size: 16px;
|
11
|
+
}
|
12
|
+
|
13
|
+
.green {
|
14
|
+
background-color: #4CAF50
|
15
|
+
}
|
16
|
+
|
17
|
+
.red {
|
18
|
+
background-color: #AF4C50
|
19
|
+
}
|
20
|
+
|
21
|
+
.blue {
|
22
|
+
background-color: #4C50AF
|
23
|
+
}
|
24
|
+
|
25
|
+
.header {
|
26
|
+
padding: 10px 5px 10px 5px;
|
27
|
+
text-align: center;
|
28
|
+
color: black;
|
29
|
+
font-size: 25px;
|
30
|
+
}
|
31
|
+
|
32
|
+
.hint {
|
33
|
+
padding: 10px 5px 10px 5px;
|
34
|
+
text-align: center;
|
35
|
+
background-color: #EFFA93;
|
36
|
+
box-shadow: 5px 5px 5px;
|
37
|
+
}
|
38
|
+
|
39
|
+
.block {
|
40
|
+
padding: 30px 5px 30px 5px;
|
41
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<link rel="stylesheet" href="popup.css">
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<h2 class="header" id="mainTitle">Right now you are defining nothing. So no button will work</h2>
|
8
|
+
<h3 class="hint" id="hint">But you can click it anyway :)</h3>
|
9
|
+
<div class="block">
|
10
|
+
<input class="blue button" id="watchSwitch" type="button" value="Do not watch"/>
|
11
|
+
<input class="blue button" id="mode" type="button" value="Adding mode is enabled"/>
|
12
|
+
<input class="red button" id="clearMark" type="button" value="Clear selection"/>
|
13
|
+
<input class="green button" id="confirm" type="button" value="Confirm selection"/>
|
14
|
+
</div>
|
15
|
+
<script src="popup.js"></script>
|
16
|
+
</body>
|
17
|
+
</html>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
async function get_value(name) {
|
2
|
+
let x = await new Promise((resolve, reject) => chrome.storage.sync.get([name], resolve));
|
3
|
+
return x[name];
|
4
|
+
};
|
5
|
+
|
6
|
+
async function set_value(name, value){
|
7
|
+
let temp = {};
|
8
|
+
temp[name] = value;
|
9
|
+
await chrome.storage.sync.set(temp, function() {});
|
10
|
+
};
|
11
|
+
|
12
|
+
async function correct_buttons() {
|
13
|
+
if (await get_value("magic_div") === true) {
|
14
|
+
document.getElementById("watchSwitch").setAttribute("value", "Locatine is waiting for click");
|
15
|
+
} else {
|
16
|
+
document.getElementById("watchSwitch").setAttribute("value", "Locatine is not waiting now");
|
17
|
+
};
|
18
|
+
if (await get_value("locatine_collection") === true) {
|
19
|
+
document.getElementById("mode").setAttribute("value", "You are in collection mode")
|
20
|
+
} else {
|
21
|
+
document.getElementById("mode").setAttribute("value", "You are in single selection mode")
|
22
|
+
};
|
23
|
+
document.getElementById("mainTitle").innerText = await get_value("locatine_title");
|
24
|
+
document.getElementById("hint").innerText = await get_value("locatine_hint");
|
25
|
+
}
|
26
|
+
|
27
|
+
async function watch() {
|
28
|
+
await set_value("magic_div", !(await get_value("magic_div")));
|
29
|
+
}
|
30
|
+
|
31
|
+
function clear() {
|
32
|
+
set_value("locatine_confirm", "declined");
|
33
|
+
}
|
34
|
+
|
35
|
+
function confirm() {
|
36
|
+
set_value("locatine_confirm", true);
|
37
|
+
}
|
38
|
+
|
39
|
+
async function mode() {
|
40
|
+
await set_value("locatine_collection", !(await get_value("locatine_collection")));
|
41
|
+
}
|
42
|
+
|
43
|
+
document.getElementById("watchSwitch").onclick = function() {watch()};
|
44
|
+
document.getElementById("clearMark").onclick = function() {clear()};
|
45
|
+
document.getElementById("confirm").onclick = function() {confirm()};
|
46
|
+
document.getElementById("mode").onclick = function() {mode()};
|
47
|
+
|
48
|
+
setInterval(function(){
|
49
|
+
correct_buttons();
|
50
|
+
}, 100);
|
@@ -0,0 +1,601 @@
|
|
1
|
+
require "watir"
|
2
|
+
require "json"
|
3
|
+
require "fileutils"
|
4
|
+
require "chromedriver-helper"
|
5
|
+
|
6
|
+
module Locatine
|
7
|
+
|
8
|
+
##
|
9
|
+
# Search is the main class of the Locatine
|
10
|
+
#
|
11
|
+
# Locatine can search.
|
12
|
+
class Search
|
13
|
+
|
14
|
+
attr_accessor :data, :depth, :browser, :learn, :json, :stability_limit, :scope
|
15
|
+
|
16
|
+
##
|
17
|
+
# Creates a new instance of Search
|
18
|
+
#
|
19
|
+
# Params:
|
20
|
+
# +json+ is the name of file to store//read data. Default => "./Locatine_files/default.json"
|
21
|
+
#
|
22
|
+
# +depth+ is the value that shows how many data will be stored for element.
|
23
|
+
#
|
24
|
+
# +browser+ is the instance of Watir::Browser. Unless provided it gonna be created with locatine-app onboard.
|
25
|
+
#
|
26
|
+
# +learn+ shows will locatine ask for assistance from user or will fail on error. learn is true when LEARN parameter is set in environment.
|
27
|
+
#
|
28
|
+
# +stability_limit+ shows max times attribute should be present to consider it trusted.
|
29
|
+
#
|
30
|
+
# +scope+ will be used in search (if not provided) defaulkt is "Default"
|
31
|
+
def initialize(json: "./Locatine_files/default.json",
|
32
|
+
depth: 3,
|
33
|
+
browser: nil,
|
34
|
+
learn: ENV['LEARN'].nil? ? false : true,
|
35
|
+
stability_limit: 10,
|
36
|
+
scope: "Default")
|
37
|
+
if !browser
|
38
|
+
@browser = Watir::Browser.new(:chrome, switches: ["--load-extension=#{HOME}/app"])
|
39
|
+
else
|
40
|
+
@browser = browser
|
41
|
+
end
|
42
|
+
@json = json
|
43
|
+
@folder = File.dirname(@json)
|
44
|
+
@name = File.basename(@json)
|
45
|
+
@depth = depth
|
46
|
+
@data = read_create
|
47
|
+
@learn = learn
|
48
|
+
@stability_limit = stability_limit
|
49
|
+
@scope = scope
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Looking for the element
|
54
|
+
#
|
55
|
+
# Params:
|
56
|
+
#
|
57
|
+
# +scope+ is a parameter that is used to get information about the element from @data. Default is "Default"
|
58
|
+
#
|
59
|
+
# +name+ is a parameter that is used to get information about the element from @data. Must not be nil.
|
60
|
+
#
|
61
|
+
# +exact+ if true locatine will be forced to use only basic search. Default is false
|
62
|
+
#
|
63
|
+
# +locator+ if not empty it is used for the first attempt to find the element. Default is {}
|
64
|
+
#
|
65
|
+
# +vars+ hash of variables that will be used for dynamic attributes. See readme for example
|
66
|
+
#
|
67
|
+
# +look_in+ only elements of that kind will be used. Use Watir::Browser methods returning collections (:text_fields, :links, :divs, etc.)
|
68
|
+
#
|
69
|
+
# +iframe+ if provided locatine will look for elements inside of it
|
70
|
+
def find(simple_name = nil,
|
71
|
+
name: nil,
|
72
|
+
scope: nil,
|
73
|
+
exact: false,
|
74
|
+
locator: {},
|
75
|
+
vars: {},
|
76
|
+
look_in: nil,
|
77
|
+
iframe: nil,
|
78
|
+
return_locator: false,
|
79
|
+
collection: false)
|
80
|
+
name ||= simple_name
|
81
|
+
raise ArgumentError, ":name should be provided" if !name
|
82
|
+
@type = look_in
|
83
|
+
@iframe = iframe
|
84
|
+
scope = @scope if scope.nil?
|
85
|
+
scope = "Default" if scope.nil?
|
86
|
+
result = find_by_locator(locator) if locator != {}
|
87
|
+
if !result
|
88
|
+
if @data[scope][name].to_h != {}
|
89
|
+
result = find_by_data(@data[scope][name], vars)
|
90
|
+
attributes = generate_data(result, vars) if result
|
91
|
+
if !result && !exact
|
92
|
+
result, attributes = find_by_magic(name, scope, @data[scope][name], vars)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
result, attributes = ask(scope, name, result, vars) if @learn
|
97
|
+
raise RuntimeError, "Nothing was found for #{scope} #{name}" if !result && !exact
|
98
|
+
if result
|
99
|
+
attributes = generate_data(result, vars) if !attributes
|
100
|
+
store(attributes, scope, name)
|
101
|
+
return return_locator ? {xpath: generate_xpath(attributes, vars)} : to_subtype(result, collection)
|
102
|
+
else
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
# Find alias with return_locator option enforced
|
109
|
+
def lctr(*args)
|
110
|
+
if args.last.class == Hash
|
111
|
+
args.last[:return_locator] = true
|
112
|
+
else
|
113
|
+
args.push({return_locator: true})
|
114
|
+
end
|
115
|
+
find(args)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Find alias with collection option enforced
|
120
|
+
def collect(*args)
|
121
|
+
enforce(:collection, true, *args)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
##
|
127
|
+
# Reading data from provided file which is set on init of the class instance
|
128
|
+
#
|
129
|
+
# If there is no dir or\and file they will be created
|
130
|
+
def read_create
|
131
|
+
unless File.directory?(@folder)
|
132
|
+
FileUtils.mkdir_p(@folder)
|
133
|
+
end
|
134
|
+
if File.exists?(@json)
|
135
|
+
hash = Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = {}}}
|
136
|
+
return hash.merge(JSON.parse(File.read(@json))["data"])
|
137
|
+
else
|
138
|
+
f = File.new(@json, "w")
|
139
|
+
f.puts '{"data" : {}}'
|
140
|
+
f.close
|
141
|
+
return Hash.new { |hash, key| hash[key] = Hash.new { |hash, key| hash[key] = {}}}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def enforce(what, value, *args)
|
146
|
+
if args.last.class == Hash
|
147
|
+
args.last[what] = value
|
148
|
+
find(*args)
|
149
|
+
else
|
150
|
+
temp = Hash.new
|
151
|
+
temp[what] = value
|
152
|
+
args.push(temp)
|
153
|
+
find(*args)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def engine
|
158
|
+
return (@iframe || @browser)
|
159
|
+
end
|
160
|
+
|
161
|
+
def collection?(the_class)
|
162
|
+
case the_class.superclass.to_s
|
163
|
+
when "Object"
|
164
|
+
return nil
|
165
|
+
when "Watir::Element"
|
166
|
+
return false
|
167
|
+
when "Watir::ElementCollection"
|
168
|
+
return true
|
169
|
+
else
|
170
|
+
return collection?(the_class.superclass)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Getting all the elements matching a locator
|
176
|
+
def find_by_locator(locator)
|
177
|
+
method = @type.nil? ? :elements : @type
|
178
|
+
results = engine.send(method, locator)
|
179
|
+
case collection?(results.class)
|
180
|
+
when nil
|
181
|
+
@type = nil
|
182
|
+
raise ArgumentError, "#{method} is not good for :look_in property. Use a method of Watir::Browser that returns a collection (like :divs, :links, etc.)"
|
183
|
+
when true
|
184
|
+
begin
|
185
|
+
results[0].wait_until(timeout: @cold_time) { |el| el.present? }
|
186
|
+
return results
|
187
|
+
rescue
|
188
|
+
return nil
|
189
|
+
end
|
190
|
+
when false
|
191
|
+
begin
|
192
|
+
warn "#{method} works for :look_in. But it is better to use a method of Watir::Browser that returns a collection (like :divs, :links, etc.)"
|
193
|
+
results.wait_until(timeout: @cold_time) { |el| el.present? }
|
194
|
+
the_class = results.class
|
195
|
+
results = engine.elements(locator).to_a.select{|item| item.to_subtype.class == the_class}
|
196
|
+
return results
|
197
|
+
rescue
|
198
|
+
return nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def get_trusted(array)
|
204
|
+
if array.length > 0
|
205
|
+
max_stability = (array.max_by {|i| i["stability"].to_i})["stability"].to_i
|
206
|
+
return (array.select {|i| i["stability"].to_i == max_stability}).uniq
|
207
|
+
else
|
208
|
+
return []
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def generate_xpath(data, vars)
|
213
|
+
xpath = ''
|
214
|
+
data.each_pair do |depth, array|
|
215
|
+
trusted = get_trusted(array)
|
216
|
+
trusted.each do |hash|
|
217
|
+
case hash["type"]
|
218
|
+
when "tag"
|
219
|
+
xpath = "[self::#{process_string(hash["value"], vars)}]" + xpath
|
220
|
+
when "text"
|
221
|
+
xpath = "[contains(text(), '#{process_string(hash["value"], vars)}')]" + xpath
|
222
|
+
when "attribute"
|
223
|
+
full_part = "[@*"
|
224
|
+
hash["name"].split("_").each do |part|
|
225
|
+
full_part = full_part + "[contains(name(), '#{part}')]"
|
226
|
+
end
|
227
|
+
xpath = full_part + "[contains(., '#{process_string(hash["value"], vars)}')]]" + xpath
|
228
|
+
end
|
229
|
+
end
|
230
|
+
xpath = '/*' + xpath
|
231
|
+
end
|
232
|
+
xpath = '/' + xpath
|
233
|
+
return xpath
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Getting all the elements via stored information
|
238
|
+
def find_by_data(data, vars)
|
239
|
+
find_by_locator({xpath: generate_xpath(data, vars)})
|
240
|
+
end
|
241
|
+
|
242
|
+
##
|
243
|
+
# Getting all the elements via black magic
|
244
|
+
def find_by_magic(name, scope, data, vars)
|
245
|
+
warn "Cannot locate #{name} in #{scope} with usual ways. Trying to use magic"
|
246
|
+
all = []
|
247
|
+
timeout = @cold_time
|
248
|
+
@cold_time = 0
|
249
|
+
data.each_pair do |depth, array|
|
250
|
+
trusted = get_trusted(array)
|
251
|
+
trusted.each do |hash|
|
252
|
+
case hash["type"]
|
253
|
+
when "tag"
|
254
|
+
all = all + find_by_tag(hash, vars, depth).to_a
|
255
|
+
when "text"
|
256
|
+
all = all + find_by_text(hash, vars, depth).to_a
|
257
|
+
when "attribute"
|
258
|
+
all = all + find_by_attribute(hash, vars, depth).to_a
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
@cold_time = timeout
|
263
|
+
raise RuntimeError, "Locatine is unable to find element #{name} in #{scope}" if all.length == 0
|
264
|
+
# Something esoteric here :)
|
265
|
+
max = all.count(all.max_by {|i| all.count(i)})
|
266
|
+
suggestion = (all.select {|i| all.count(i) == max}).uniq
|
267
|
+
attributes = generate_data(suggestion, vars)
|
268
|
+
return suggestion, attributes
|
269
|
+
end
|
270
|
+
|
271
|
+
##
|
272
|
+
# Getting elements by attribute
|
273
|
+
def find_by_attribute(hash, vars, depth = 0)
|
274
|
+
correction = "/*" * depth.to_i
|
275
|
+
full_part = "//*[@*"
|
276
|
+
hash["name"].split("_").each do |part|
|
277
|
+
full_part = full_part + "[contains(name(), '#{part}')]"
|
278
|
+
end
|
279
|
+
xpath = full_part + "[., '#{process_string(hash["value"], vars)}')]]"
|
280
|
+
find_by_locator(xpath: "#{full_part}[contains(., '#{process_string(hash["value"], vars)}')]]#{correction}")
|
281
|
+
end
|
282
|
+
|
283
|
+
##
|
284
|
+
# Getting elements by tag
|
285
|
+
def find_by_tag(hash, vars, depth = 0)
|
286
|
+
correction = "/*" * depth.to_i
|
287
|
+
find_by_locator(xpath: "//*[self::#{process_string(hash["value"], vars)}')]#{correction}")
|
288
|
+
end
|
289
|
+
|
290
|
+
##
|
291
|
+
# Getting elements by text
|
292
|
+
def find_by_text(hash, vars, depth = 0)
|
293
|
+
correction = "/*" * depth.to_i
|
294
|
+
find_by_locator(xpath: "//*[contains(text(), '#{process_string(hash["value"], vars)}')]#{correction}")
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Setting attribute of locatine div (way to communicate)
|
299
|
+
def send_to_app(what, value, b = engine)
|
300
|
+
fix_iframe
|
301
|
+
b.wd.execute_script(%Q[if (document.getElementById('locatine_magic_div')) {
|
302
|
+
return document.getElementById('locatine_magic_div').setAttribute("#{what}", "#{value}")}])
|
303
|
+
fix_iframe
|
304
|
+
end
|
305
|
+
|
306
|
+
##
|
307
|
+
# Getting attribute of locatine div (way to communicate)
|
308
|
+
def get_from_app(what)
|
309
|
+
fix_iframe
|
310
|
+
result = engine.wd.execute_script(%Q[if (document.getElementById('locatine_magic_div')) {
|
311
|
+
return document.getElementById('locatine_magic_div').getAttribute("#{what}")}])
|
312
|
+
fix_iframe
|
313
|
+
return result
|
314
|
+
end
|
315
|
+
|
316
|
+
def fix_iframe
|
317
|
+
if @iframe
|
318
|
+
@iframe = @browser.iframe(@iframe.selector)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def set_title(text)
|
323
|
+
puts text
|
324
|
+
send_to_app("locatinetitle", text)
|
325
|
+
end
|
326
|
+
|
327
|
+
##
|
328
|
+
# Sending request to locatine app
|
329
|
+
def start_listening(scope, name)
|
330
|
+
send_to_app("locatinestyle", "blocked", @browser) if @iframe
|
331
|
+
send_to_app("locatinehint", "Toggle single//collection mode button if you need. If you want to do some actions on the page toggle Locatine waiting button. You also can select element on devtools -> Elements. Do not forget to confirm your selection.")
|
332
|
+
send_to_app("locatinestyle", "set_true")
|
333
|
+
sleep 0.5
|
334
|
+
end
|
335
|
+
|
336
|
+
def find_by_guess(scope, name, vars)
|
337
|
+
all = []
|
338
|
+
timeout = @cold_time
|
339
|
+
@cold_time = 0
|
340
|
+
name.split(" ").each do |part|
|
341
|
+
all = all + find_by_locator({tag_name: part}).to_a
|
342
|
+
all = all + find_by_locator({xpath: "//*[contains(text(),'#{part}')]"}).to_a
|
343
|
+
all = all + find_by_locator({xpath: "//*[@*[contains(., '#{part}')]]"}).to_a
|
344
|
+
end
|
345
|
+
if all.length>0
|
346
|
+
max = all.count(all.max_by {|i| all.count(i)})
|
347
|
+
guess = (all.select {|i| all.count(i) == max}).uniq
|
348
|
+
guess_data = generate_data(guess, vars)
|
349
|
+
by_data = find_by_data(guess_data, vars)
|
350
|
+
if by_data.nil? || (engine.elements.length/find_by_data(guess_data, vars).length <=4)
|
351
|
+
set_title "Locatine has no good guess for #{name} in #{scope}. Try to change the name. Or just define it."
|
352
|
+
guess = nil
|
353
|
+
guess_data = {}
|
354
|
+
end
|
355
|
+
else
|
356
|
+
set_title "Locatine has no guess for #{name} in #{scope}. Try to change the name. Or just define it."
|
357
|
+
end
|
358
|
+
@cold_time = timeout
|
359
|
+
return guess, guess_data.to_h
|
360
|
+
end
|
361
|
+
|
362
|
+
##
|
363
|
+
# request send and waiting for an answer
|
364
|
+
def ask(scope, name, result, vars)
|
365
|
+
start_listening(scope, name)
|
366
|
+
element, attributes, finished, old_tag, old_index, old_element = result, {}, false, nil, nil, nil
|
367
|
+
if !element.nil?
|
368
|
+
attributes = generate_data(element, vars)
|
369
|
+
else
|
370
|
+
set_title("Locatine is trying to guess what is #{name} in #{scope}.")
|
371
|
+
element, attributes = find_by_guess(scope, name, vars) if name.length >= 5
|
372
|
+
end
|
373
|
+
while !finished do
|
374
|
+
sleep 0.1
|
375
|
+
tag = get_from_app("tag")
|
376
|
+
tag = tag.downcase if !tag.nil?
|
377
|
+
index = get_from_app("index").to_i
|
378
|
+
if (!tag.to_s.strip.empty?) && ((tag != old_tag) or (old_index != index))
|
379
|
+
element = [engine.elements({tag_name: tag})[index]]
|
380
|
+
new_attributes = generate_data(element, vars)
|
381
|
+
if get_from_app("locatinecollection") == "true"
|
382
|
+
attributes = get_commons(new_attributes, attributes)
|
383
|
+
element = find_by_data(attributes, vars)
|
384
|
+
else
|
385
|
+
attributes = new_attributes
|
386
|
+
end
|
387
|
+
end
|
388
|
+
if old_element != element
|
389
|
+
mass_highlight_turn(old_element, false) if old_element
|
390
|
+
mass_highlight_turn(element) if element
|
391
|
+
if element.nil?
|
392
|
+
set_title "Nothing is selected as #{name} in #{scope}"
|
393
|
+
else
|
394
|
+
set_title "#{element.length} elements were selected as #{name} in #{scope}. If it is correct - confirm the selection."
|
395
|
+
end
|
396
|
+
end
|
397
|
+
old_element, old_tag, old_index = element, tag, index
|
398
|
+
case get_from_app("locatineconfirmed")
|
399
|
+
when "true"
|
400
|
+
send_to_app("locatineconfirmed", "ok")
|
401
|
+
send_to_app("locatinetitle", "Right now you are defining nothing. So no button will work")
|
402
|
+
send_to_app("locatinehint", "Place for a smart hint here")
|
403
|
+
finished = true
|
404
|
+
when "declined"
|
405
|
+
send_to_app("locatineconfirmed", "ok")
|
406
|
+
element, old_tag, old_index, tag, index, attributes = nil, nil, nil, nil, nil, {}
|
407
|
+
end
|
408
|
+
end
|
409
|
+
mass_highlight_turn(element, false)
|
410
|
+
send_to_app("locatinestyle", "ok", @browser) if @iframe
|
411
|
+
sleep 0.5
|
412
|
+
return element, attributes
|
413
|
+
end
|
414
|
+
|
415
|
+
##
|
416
|
+
# We can highlight an element
|
417
|
+
def highlight(element)
|
418
|
+
if !element.stale? && element.exists?
|
419
|
+
begin
|
420
|
+
engine.execute_script("arguments[0].setAttribute"\
|
421
|
+
"('locatineclass','foundbylocatine')", element)
|
422
|
+
rescue
|
423
|
+
warn " something was found as #{element.selector} but we cannot highlight it"
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
##
|
429
|
+
# We can unhighlight an element
|
430
|
+
def unhighlight(element)
|
431
|
+
if !element.stale? && element.exists?
|
432
|
+
begin
|
433
|
+
engine.execute_script("arguments[0].removeAttribute('locatineclass')",
|
434
|
+
element)
|
435
|
+
rescue
|
436
|
+
# watir is not allowing to play with attributes of some strange elements
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
##
|
442
|
+
# We can highlight\unhighlight tons of elements at once
|
443
|
+
def mass_highlight_turn(mass, turn_on = true)
|
444
|
+
mass.each do |element|
|
445
|
+
if turn_on
|
446
|
+
highlight element
|
447
|
+
else
|
448
|
+
unhighlight element
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
##
|
454
|
+
# Generating array of hashes representing data of the element
|
455
|
+
def get_element_info(element, vars)
|
456
|
+
attrs = []
|
457
|
+
get_attributes(element).each do |hash|
|
458
|
+
if vars[hash["name"].to_sym]
|
459
|
+
hash["value"].gsub!(vars[hash["name"].to_sym], "\#{#{hash["name"]}}")
|
460
|
+
end
|
461
|
+
attrs.push hash
|
462
|
+
end
|
463
|
+
txt = (element.text == element.inner_html) ? element.text : ''
|
464
|
+
tag = element.tag_name
|
465
|
+
if vars[:tag] == tag
|
466
|
+
tag = "\#{tag}"
|
467
|
+
end
|
468
|
+
attrs.push({"name" => "tag", "value" => tag, "type" => "tag"})
|
469
|
+
txt.split(" ").each do |word|
|
470
|
+
if !vars[:text].to_s.strip.empty?
|
471
|
+
final_word = word.gsub(vars[:text].to_s, "\#{text}")
|
472
|
+
else
|
473
|
+
final_word = word
|
474
|
+
end
|
475
|
+
attrs.push({"name" => "text", "value" => final_word, "type" => "text"})
|
476
|
+
end
|
477
|
+
return attrs
|
478
|
+
end
|
479
|
+
|
480
|
+
##
|
481
|
+
# Merging data of two elements (new data is to find both)
|
482
|
+
def get_commons(first, second)
|
483
|
+
second = first if second == {}
|
484
|
+
final = Hash.new { |hash, key| hash[key] = [] }
|
485
|
+
first.each_pair do |depth, array|
|
486
|
+
array.each do |hash|
|
487
|
+
to_add = second[depth].select {|item| (item["name"] == hash["name"]) and (item["value"] == hash["value"]) and item["type"] == hash["type"]}
|
488
|
+
final[depth] = final[depth] + to_add
|
489
|
+
end
|
490
|
+
end
|
491
|
+
final
|
492
|
+
end
|
493
|
+
|
494
|
+
##
|
495
|
+
# Setting stability
|
496
|
+
def set_stability(first, second)
|
497
|
+
second = first if second.to_h == {}
|
498
|
+
final = Hash.new { |hash, key| hash[key] = [] }
|
499
|
+
first.each_pair do |depth, array|
|
500
|
+
array.each do |hash|
|
501
|
+
to_add = second[depth].select {|item| (item["name"] == hash["name"]) and (item["value"] == hash["value"]) and item["type"] == hash["type"]}
|
502
|
+
if to_add.length > 0 # old ones
|
503
|
+
to_add[0]["stability"] = (to_add[0]["stability"].to_i + 1).to_s if (to_add[0]["stability"].to_i < @stability_limit)
|
504
|
+
final[depth] = final[depth] + to_add
|
505
|
+
else # new ones
|
506
|
+
hash["stability"] = "1"
|
507
|
+
final[depth] = final[depth].push hash
|
508
|
+
end
|
509
|
+
end
|
510
|
+
final[depth].uniq!
|
511
|
+
end
|
512
|
+
final
|
513
|
+
end
|
514
|
+
|
515
|
+
##
|
516
|
+
# Generating data for group of elements
|
517
|
+
def generate_data(result, vars)
|
518
|
+
family = {}
|
519
|
+
result.each do |item|
|
520
|
+
family = get_commons(get_family_info(item, vars), family)
|
521
|
+
end
|
522
|
+
return family
|
523
|
+
end
|
524
|
+
|
525
|
+
##
|
526
|
+
# Getting element\\parents information
|
527
|
+
def get_family_info(element, vars)
|
528
|
+
current_depth = 0
|
529
|
+
attributes = {};
|
530
|
+
while current_depth != @depth
|
531
|
+
attributes[current_depth.to_s] = get_element_info(element, vars)
|
532
|
+
current_depth = current_depth+1
|
533
|
+
element = element.parent
|
534
|
+
# Sometimes watir is not returning a valid parent that's why:
|
535
|
+
current_depth = @depth if !element.parent.exists?
|
536
|
+
end
|
537
|
+
return attributes
|
538
|
+
end
|
539
|
+
|
540
|
+
##
|
541
|
+
# Saving json
|
542
|
+
def store(attributes, scope, name)
|
543
|
+
@data[scope][name] = set_stability(attributes, @data[scope][name])
|
544
|
+
to_write = ({"data" => @data})
|
545
|
+
File.open(@json, "w") do |f|
|
546
|
+
f.write(JSON.pretty_generate(to_write))
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
##
|
551
|
+
# Collecting attributes of the element
|
552
|
+
def get_attributes(element)
|
553
|
+
attributes = element.attributes
|
554
|
+
array = Array.new
|
555
|
+
attributes.each_pair do |name, value|
|
556
|
+
if (name.to_s != "locatineclass")
|
557
|
+
value.split(" ").uniq.each do |part|
|
558
|
+
array.push({"name" => name.to_s, "type" => "attribute", "value" => part})
|
559
|
+
end
|
560
|
+
end
|
561
|
+
end
|
562
|
+
return array
|
563
|
+
end
|
564
|
+
|
565
|
+
##
|
566
|
+
# Replacing dynamic entries with values
|
567
|
+
def process_string(str, vars)
|
568
|
+
str ||= ""
|
569
|
+
n = nil
|
570
|
+
while str != n
|
571
|
+
str = n if !n.nil?
|
572
|
+
thevar = str.match(/\#{[^\#{]*}/).to_s
|
573
|
+
if thevar != ""
|
574
|
+
value = vars[thevar.match(/(\w.*)}/)[1].to_sym]
|
575
|
+
raise ArgumentError, ":#{thevar.match(/(\w.*)}/)[1]} must be provided in vars since element was defined with it" if !value
|
576
|
+
n = str.gsub(thevar, value)
|
577
|
+
else
|
578
|
+
n = str
|
579
|
+
end
|
580
|
+
end
|
581
|
+
str
|
582
|
+
end
|
583
|
+
|
584
|
+
##
|
585
|
+
# Returning subtype of the only element of collection OR collection
|
586
|
+
#
|
587
|
+
# Params:
|
588
|
+
# +result+ must be Watir::HTMLElementCollection or Array
|
589
|
+
#
|
590
|
+
# +collection+ nil, true or false
|
591
|
+
def to_subtype(result, collection)
|
592
|
+
case collection
|
593
|
+
when true
|
594
|
+
return result
|
595
|
+
when false
|
596
|
+
return result.first.to_subtype
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
end
|
data/lib/locatine.rb
ADDED
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: locatine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.0092'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sergei Seleznev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simplecov
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: watir
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '6.16'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '6.16'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: chromedriver-helper
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.0'
|
111
|
+
description: The main goal to write locators never
|
112
|
+
email: s_seleznev_qa@hotmail.com
|
113
|
+
executables: []
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files: []
|
116
|
+
files:
|
117
|
+
- lib/locatine.rb
|
118
|
+
- lib/locatine/app/background.js
|
119
|
+
- lib/locatine/app/content.css
|
120
|
+
- lib/locatine/app/content.js
|
121
|
+
- lib/locatine/app/devtools.html
|
122
|
+
- lib/locatine/app/devtools.js
|
123
|
+
- lib/locatine/app/manifest.json
|
124
|
+
- lib/locatine/app/popup.css
|
125
|
+
- lib/locatine/app/popup.html
|
126
|
+
- lib/locatine/app/popup.js
|
127
|
+
- lib/locatine/search.rb
|
128
|
+
- lib/locatine/version.rb
|
129
|
+
homepage: https://github.com/sseleznevqa/locatine
|
130
|
+
licenses:
|
131
|
+
- MIT
|
132
|
+
metadata: {}
|
133
|
+
post_install_message:
|
134
|
+
rdoc_options: []
|
135
|
+
require_paths:
|
136
|
+
- lib
|
137
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
requirements: []
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 2.5.2.3
|
150
|
+
signing_key:
|
151
|
+
specification_version: 4
|
152
|
+
summary: Element locating tool based on watir
|
153
|
+
test_files: []
|