locatine 0.0092
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.
- 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: []
|