tellurium_driver 1.1.23 → 1.2.3
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 +4 -4
- data/lib/tellurium_driver.rb +250 -105
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e22bbe5f3a58319caa758ee2e4cf66a66c9d4d7e
|
4
|
+
data.tar.gz: 583775721267ca37ac90b0663e6f75c97a001e0d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 38c2723d74b211441af599c740292032bf6bdb363876a0b282d62b33ee7d4078ac5404dc818fd4869224939f72a8ad75c9f4472d34b919fbfc508adf9e658d5c
|
7
|
+
data.tar.gz: 4d995672f18aeb2c351d69c07210fac90e2d4cedea9f8d31280f04caca14cb2cd77d05c608c662d57413c1304daf7c2d0594af2c2efed2fff6af62a82f9680a2
|
data/lib/tellurium_driver.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
#Provides added functionality to Selenium WebDriver
|
1
|
+
# Provides added functionality to Selenium WebDriver
|
2
|
+
#
|
3
|
+
# Author:: Noah Prince (mailto:noahprince8@gmail.com)
|
2
4
|
class TelluriumDriver
|
3
5
|
class << self
|
4
6
|
attr_accessor :wait_for_document_ready
|
5
7
|
end
|
6
|
-
|
7
|
-
#
|
8
|
+
|
9
|
+
# :nodoc:
|
8
10
|
def self.before(names)
|
9
11
|
names.each do |name|
|
10
12
|
m = instance_method(name)
|
@@ -14,47 +16,46 @@ class TelluriumDriver
|
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
19
|
+
# :doc:
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
# Sets up this instance of TelluriumDriver
|
22
|
+
#
|
23
|
+
# ==== Options
|
24
|
+
#
|
25
|
+
# * +:browser+ - "chrome","firefox", "safari" or "internet explorer"
|
26
|
+
# * +:version+ - [String] the version of the browser. Not needed for safari, chrome, or firefox.
|
27
|
+
# * +:hub_ip+ - the IP address of the Selenium Grid hub to test on.
|
28
|
+
# * Will use http://hub_ip:4444/wd/hub. If this is not correct, use :hub_url
|
29
|
+
# * +:hub_url+ - the full url address of the Selenium Grid hub to test on
|
30
|
+
# * WARNING: DO NOT USE BOTH hub_ip and hub_url
|
31
|
+
# * NOTE: If either hub_ip or hub_url is present, tests will not run locally
|
32
|
+
# * +:caps+ - see https://code.google.com/p/selenium/wiki/DesiredCapabilities
|
33
|
+
# * Not necessary, but will give the browser any extra desired functionality
|
34
|
+
# * +:timeout+ - Number of seconds for all Tellurium wait commands. Default 120
|
35
|
+
#
|
36
|
+
# ==== Examples
|
37
|
+
#
|
38
|
+
# Run a local chrome instance
|
39
|
+
# TelluriumDriver.new(browser: "chrome")
|
40
|
+
#
|
41
|
+
# Run an IE10 instance on the grid with ip 192.168.1.1
|
42
|
+
# TelluriumDriver.new(browser: "internet explorer", version: "10", hub_ip: 192.168.1.1)
|
43
|
+
def initialize(opts = {})
|
44
|
+
opts[:timeout] = 120 unless opts[:timeout]
|
45
|
+
@wait = Selenium::WebDriver::Wait.new(:timeout=>opts[:timeout])
|
27
46
|
TelluriumDriver.wait_for_document_ready=true;
|
47
|
+
|
48
|
+
opts[:caps] ||= {}
|
49
|
+
opts[:caps][:browserName] ||= opts[:browser]
|
50
|
+
opts[:caps][:version] ||= opts[:version]
|
51
|
+
|
52
|
+
is_local = !opts[:hub_ip] and !opts[:hub_url]
|
28
53
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
if is_chrome && is_local
|
35
|
-
caps = {
|
36
|
-
:browserName => "chrome",
|
37
|
-
:idleTimeout => timeout
|
38
|
-
# :screenshot => true
|
39
|
-
}
|
40
|
-
@driver = Selenium::WebDriver.for :chrome,:desired_capabilities=>caps
|
41
|
-
elsif is_firefox && is_local
|
42
|
-
caps = {
|
43
|
-
:browserName => "firefox",
|
44
|
-
:idleTimeout => timeout,
|
45
|
-
:screenshot => true
|
46
|
-
}
|
47
|
-
@driver = Selenium::WebDriver.for :firefox, :desired_capabilities=>caps
|
48
|
-
elsif is_ie
|
49
|
-
caps = Selenium::WebDriver::Remote::Capabilities.internet_explorer
|
50
|
-
caps.version = version.to_s
|
51
|
-
@driver = Selenium::WebDriver.for(:remote,:desired_capabilities=>caps,:url=> "http://#{hub_ip}:4444/wd/hub")
|
52
|
-
elsif is_chrome
|
53
|
-
@driver = Selenium::WebDriver.for(:remote,:desired_capabilities=>:chrome,:url=> "http://#{hub_ip}:4444/wd/hub")
|
54
|
-
elsif is_firefox
|
55
|
-
@driver = Selenium::WebDriver.for(:remote,:desired_capabilities=>:firefox,:url=> "http://#{hub_ip}:4444/wd/hub")
|
54
|
+
if is_local
|
55
|
+
@driver = Selenium::WebDriver.for(opts[:browser].to_sym,:desired_capabilities=>opts[:caps])
|
56
|
+
else
|
57
|
+
@driver = Selenium::WebDriver.for(:remote,:desired_capabilities=>opts[:caps],:url=> "http://#{opts[:hub_ip]}:4444/wd/hub")
|
56
58
|
end
|
57
|
-
|
58
59
|
end
|
59
60
|
|
60
61
|
|
@@ -78,62 +79,51 @@ class TelluriumDriver
|
|
78
79
|
@driver.send sym,*args,&block
|
79
80
|
end
|
80
81
|
|
81
|
-
|
82
|
+
# Goes to a given url, does not wait for load
|
83
|
+
#
|
84
|
+
# ==== Attributes
|
85
|
+
#
|
86
|
+
# * +url+ - the url to visit
|
82
87
|
def go_to(url)
|
83
88
|
driver.get url
|
84
89
|
end
|
85
90
|
|
86
|
-
#
|
87
|
-
|
91
|
+
# Goes to a given url, waits for it to load
|
92
|
+
# * NOTE: To see if loaded, waits for the title of the window to change
|
93
|
+
#
|
94
|
+
# ==== Attributes
|
95
|
+
#
|
96
|
+
# * +url+ - the url to visit
|
88
97
|
def go_to_and_wait_to_load(url)
|
89
98
|
current_name = driver.title
|
90
99
|
driver.get url
|
91
100
|
|
92
|
-
#wait until the current title changes to see that you're at a new url
|
101
|
+
# wait until the current title changes to see that you're at a new url
|
93
102
|
@wait.until { driver.title != current_name }
|
94
103
|
end
|
95
|
-
|
96
|
-
#
|
97
|
-
|
104
|
+
|
105
|
+
# Goes to a given url, and waits for a given element to appear
|
106
|
+
#
|
107
|
+
# ==== Attributes
|
108
|
+
#
|
109
|
+
# * +url+ - the url to visit
|
110
|
+
# * +sym+ - :id, :name, :css, etc
|
111
|
+
# * +id+ - The text corresponding with the symbol.
|
112
|
+
#
|
113
|
+
# ==== Examples
|
114
|
+
# driver.load_url_and_wait_for_element(:id,"password")
|
115
|
+
def load_url_and_wait_for_element(url,sym,id)
|
98
116
|
current_name = driver.title
|
99
117
|
driver.get url
|
100
118
|
|
101
|
-
@wait.until { driver.title != current_name and driver.find_elements(
|
102
|
-
end
|
103
|
-
|
104
|
-
#clicks one element and waits for another one to change it's value.
|
105
|
-
#@param [String] id_to_click, the id you want to click
|
106
|
-
#@param [String] id_to_change you
|
107
|
-
def click_and_wait_to_change(id_to_click,id_to_change,value)
|
108
|
-
element_to_click = driver.find_element(:id, id_to_click)
|
109
|
-
element_to_change = driver.find_element(:id, id_to_change)
|
110
|
-
current_value = element_to_change.attribute(value.to_sym)
|
111
|
-
|
112
|
-
element_to_click.click
|
113
|
-
@wait.until { element_to_change.attribute(value.to_sym) != current_value }
|
114
|
-
end
|
115
|
-
|
116
|
-
#Fills in a value for form selectors
|
117
|
-
#@param [Symbol] sym, usually :id, :css, or :name
|
118
|
-
#@param [String] selector_value, the value to set the selector to
|
119
|
-
def click_selector(sym,id,selector_value)
|
120
|
-
option = Selenium::WebDriver::Support::Select.new(driver.find_element(sym.to_sym,id))
|
121
|
-
option.select_by(:text, selector_value)
|
122
|
-
end
|
123
|
-
|
124
|
-
#Fills out a selector and waits for another id to change
|
125
|
-
def click_selector_and_wait(id_to_click,selector_value,id_to_change,value)
|
126
|
-
element_to_change = driver.find_element(:id => id_to_change)
|
127
|
-
current_value = element_to_change.attribute(value.to_sym)
|
128
|
-
|
129
|
-
option = Selenium::WebDriver::Support::Select.new(driver.find_element(:id => id_to_click))
|
130
|
-
option.select_by(:text, selector_value)
|
131
|
-
|
132
|
-
@wait.until { element_to_change.attribute(value.to_sym) != current_value }
|
119
|
+
@wait.until { driver.title != current_name and driver.find_elements(sym, id).size > 0 }
|
133
120
|
end
|
134
121
|
|
135
|
-
#Waits for an element to be displayed
|
136
|
-
|
122
|
+
# Waits for an element to be displayed on the page
|
123
|
+
#
|
124
|
+
# ==== Attributes
|
125
|
+
#
|
126
|
+
# * +element+ - Selenium::WebDriver::Element to appear
|
137
127
|
def wait_for_element(element)
|
138
128
|
@wait.until {
|
139
129
|
bool = false
|
@@ -148,9 +138,16 @@ class TelluriumDriver
|
|
148
138
|
}
|
149
139
|
end
|
150
140
|
|
151
|
-
#Waits for the element with specified identifiers to disappear
|
152
|
-
|
153
|
-
|
141
|
+
# Waits for the element with specified identifiers to disappear
|
142
|
+
#
|
143
|
+
# ==== Attributes
|
144
|
+
#
|
145
|
+
# * +sym+ - :id, :name, :css, etc
|
146
|
+
# * +id+ - The text corresponding with the symbol.
|
147
|
+
#
|
148
|
+
# ==== Examples
|
149
|
+
#
|
150
|
+
# driver.wait_to_disappear(:id,"hover-box")
|
154
151
|
def wait_to_disappear(sym,id)
|
155
152
|
@wait.until {
|
156
153
|
element_arr = driver.find_elements(sym,id)
|
@@ -158,8 +155,18 @@ class TelluriumDriver
|
|
158
155
|
}
|
159
156
|
end
|
160
157
|
|
161
|
-
#
|
162
|
-
|
158
|
+
# Similar to wait_to_disappear, but takes an element instead of identifiers
|
159
|
+
# * WARNING: If the element disappears from the DOM, and isn't just hidden
|
160
|
+
# this will raise a stale reference error
|
161
|
+
#
|
162
|
+
# ==== Attributes
|
163
|
+
#
|
164
|
+
# * +element+ - Selenium::WebDriver::Element to appear
|
165
|
+
#
|
166
|
+
# ==== Examples
|
167
|
+
#
|
168
|
+
# element = driver.find_element(id: "foo")
|
169
|
+
# driver.wait_for_element_to_disappear(element)
|
163
170
|
def wait_for_element_to_disappear(element)
|
164
171
|
@wait.until {
|
165
172
|
begin
|
@@ -170,8 +177,16 @@ class TelluriumDriver
|
|
170
177
|
}
|
171
178
|
end
|
172
179
|
|
173
|
-
|
174
|
-
|
180
|
+
# Waits for the element with specified identifiers to appear
|
181
|
+
#
|
182
|
+
# ==== Attributes
|
183
|
+
#
|
184
|
+
# * +sym+ - :id, :name, :css, etc
|
185
|
+
# * +id+ - The text corresponding with the symbol.
|
186
|
+
#
|
187
|
+
# ==== Examples
|
188
|
+
#
|
189
|
+
# driver.wait_to_appear(:id,"hover-box")
|
175
190
|
def wait_to_appear(sym,id)
|
176
191
|
@wait.until {
|
177
192
|
element_arr = driver.find_elements(sym,id)
|
@@ -179,8 +194,16 @@ class TelluriumDriver
|
|
179
194
|
}
|
180
195
|
end
|
181
196
|
|
182
|
-
#
|
183
|
-
#
|
197
|
+
# Waits for the element with specified identifiers to appear, then clicks it
|
198
|
+
#
|
199
|
+
# ==== Attributes
|
200
|
+
#
|
201
|
+
# * +sym+ - :id, :name, :css, etc
|
202
|
+
# * +id+ - The text corresponding with the symbol.
|
203
|
+
#
|
204
|
+
# ==== Examples
|
205
|
+
#
|
206
|
+
# driver.wait_and_click(:name,"hello")
|
184
207
|
def wait_and_click(sym, id)
|
185
208
|
found_element = false
|
186
209
|
|
@@ -210,8 +233,11 @@ class TelluriumDriver
|
|
210
233
|
|
211
234
|
end
|
212
235
|
|
213
|
-
# Waits for
|
214
|
-
|
236
|
+
# Waits for the given element to appear and clicks it
|
237
|
+
#
|
238
|
+
# ==== Attributes
|
239
|
+
#
|
240
|
+
# * +element+ - Selenium::WebDriver::Element to appear
|
215
241
|
def wait_for_element_and_click(element)
|
216
242
|
wait_for_element(element)
|
217
243
|
|
@@ -227,8 +253,79 @@ class TelluriumDriver
|
|
227
253
|
|
228
254
|
end
|
229
255
|
|
230
|
-
#
|
231
|
-
|
256
|
+
# Clicks one element, and waits for the attribute of another element to change
|
257
|
+
# * NOTE: This method only takes ids
|
258
|
+
#
|
259
|
+
# ==== Attributes
|
260
|
+
#
|
261
|
+
# * +id_to_click+
|
262
|
+
# * +id_to_change+
|
263
|
+
# * +value+ - the attribute to change on the second element
|
264
|
+
#
|
265
|
+
# ==== Examples
|
266
|
+
#
|
267
|
+
# driver.click_and_wait_to_change("checkbox","score","text")
|
268
|
+
def click_and_wait_to_change(id_to_click,id_to_change,value)
|
269
|
+
element_to_click = driver.find_element(:id, id_to_click)
|
270
|
+
element_to_change = driver.find_element(:id, id_to_change)
|
271
|
+
current_value = element_to_change.attribute(value.to_sym)
|
272
|
+
|
273
|
+
element_to_click.click
|
274
|
+
@wait.until { element_to_change.attribute(value.to_sym) != current_value }
|
275
|
+
end
|
276
|
+
|
277
|
+
# Fills in a value for form selectors
|
278
|
+
#
|
279
|
+
# ==== Attributes
|
280
|
+
#
|
281
|
+
# * +sym+ - :id, :css, or :name, etc
|
282
|
+
# * +id+ - The text corresponding with the symbol.
|
283
|
+
# * +selector_value+ - The value to change the selector to
|
284
|
+
#
|
285
|
+
# ==== Examples
|
286
|
+
#
|
287
|
+
# driver.click_selector(:id,"age","21")
|
288
|
+
def click_selector(sym,id,selector_value)
|
289
|
+
option = Selenium::WebDriver::Support::Select.new(driver.find_element(sym.to_sym,id))
|
290
|
+
option.select_by(:text, selector_value)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Fills in a value for form selectors and waits for another element to change
|
294
|
+
# * NOTE: This method only uses ids
|
295
|
+
#
|
296
|
+
# ==== Attributes
|
297
|
+
#
|
298
|
+
# * +id_to_click+
|
299
|
+
# * +selector_value+ - The value to change the selector to
|
300
|
+
# * +id_to_change+
|
301
|
+
# * +value+ - the attribute to change on the second element
|
302
|
+
#
|
303
|
+
# ==== Examples
|
304
|
+
#
|
305
|
+
# Waits for the attribute "available" on the element with id: "drinks" to change
|
306
|
+
# after filling an age selector to 21
|
307
|
+
#
|
308
|
+
# driver.click_selector_and_wait("age","21","drinks","available")
|
309
|
+
def click_selector_and_wait(id_to_click,selector_value,id_to_change,value)
|
310
|
+
element_to_change = driver.find_element(:id => id_to_change)
|
311
|
+
current_value = element_to_change.attribute(value.to_sym)
|
312
|
+
|
313
|
+
option = Selenium::WebDriver::Support::Select.new(driver.find_element(:id => id_to_click))
|
314
|
+
option.select_by(:text, selector_value)
|
315
|
+
|
316
|
+
@wait.until { element_to_change.attribute(value.to_sym) != current_value }
|
317
|
+
end
|
318
|
+
|
319
|
+
# Hovers over where an element should be and clicks. Useful for hitting hidden elements
|
320
|
+
#
|
321
|
+
# ==== Attributes
|
322
|
+
#
|
323
|
+
# * +element+ - Selenium::WebDriver::Element
|
324
|
+
#
|
325
|
+
# ===== OR
|
326
|
+
#
|
327
|
+
# * +sym+ - :id, :css, or :name, etc
|
328
|
+
# * +id+ - The text corresponding with the symbol.
|
232
329
|
def hover_click(*args)
|
233
330
|
if args.size == 1
|
234
331
|
driver.action.click(element).perform
|
@@ -239,10 +336,13 @@ class TelluriumDriver
|
|
239
336
|
|
240
337
|
end
|
241
338
|
|
242
|
-
#
|
243
|
-
|
244
|
-
|
245
|
-
|
339
|
+
# Fills a text input of the given sym,id with a value
|
340
|
+
#
|
341
|
+
# ==== Attributes
|
342
|
+
#
|
343
|
+
# * +sym+ - :id, :css, or :name, etc
|
344
|
+
# * +id+ - The text corresponding with the symbol.
|
345
|
+
# * +value+ - the string to send to the text input
|
246
346
|
def send_keys(sym,id,value)
|
247
347
|
#self.wait_and_click(sym, id)
|
248
348
|
#driver.action.send_keys(driver.find_element(sym => id),value).perform
|
@@ -252,12 +352,26 @@ class TelluriumDriver
|
|
252
352
|
element.send_keys(value)
|
253
353
|
end
|
254
354
|
|
355
|
+
# :nodoc:
|
255
356
|
def send_keys_leasing(sym,id,value)
|
256
357
|
self.wait_to_appear(sym,id)
|
257
358
|
self.driver.find_element(sym,id).send_keys(value)
|
258
359
|
end
|
259
|
-
|
260
|
-
|
360
|
+
# :doc:
|
361
|
+
|
362
|
+
# Clicks one element and waits for another to exist
|
363
|
+
#
|
364
|
+
# ==== Attributes
|
365
|
+
#
|
366
|
+
# * +id1+ - Id of element to click
|
367
|
+
# * +id2+ - Id of element to exist
|
368
|
+
#
|
369
|
+
# ===== OR
|
370
|
+
#
|
371
|
+
# * +sym1+ - :id, :css, or :name, etc for the element to click
|
372
|
+
# * +id1+ - The text corresponding with the symbol.
|
373
|
+
# * +sym2+ - :id, :css, or :name, etc for the element to wait to exist
|
374
|
+
# * +id2+ - The text corresponding with the symbol.
|
261
375
|
def click_and_wait_to_exist(*args)
|
262
376
|
case args.size
|
263
377
|
when 2 #takes just two id's
|
@@ -280,22 +394,46 @@ class TelluriumDriver
|
|
280
394
|
error
|
281
395
|
end
|
282
396
|
end
|
283
|
-
|
284
|
-
|
397
|
+
|
398
|
+
# Fills out large forms via a supplied hash in the form id,value_to_send
|
399
|
+
#
|
400
|
+
# ==== Attributes
|
401
|
+
#
|
402
|
+
# * +hash+ - A hash of input IDs associated with the value to send to the input
|
403
|
+
#
|
404
|
+
# ==== Examples
|
405
|
+
#
|
406
|
+
# Fill out a login form
|
407
|
+
# driver.form_fillout({"first_name"=>"Noah",
|
408
|
+
# "last_name"=>"Prince",
|
409
|
+
# "email"=>"noahprince8@gmail.com"})
|
410
|
+
def form_fillout(hash)
|
285
411
|
hash.each do |id,value|
|
286
412
|
self.wait_to_appear(:id,id)
|
287
413
|
self.send_keys(:id,id,value)
|
288
414
|
end
|
289
415
|
|
290
416
|
end
|
291
|
-
|
417
|
+
|
418
|
+
# :nodoc:
|
292
419
|
def form_fillout_editable(selector,hash)
|
293
420
|
hash.each do |id,value|
|
294
421
|
name = "#{selector}\[#{id}\]"
|
295
422
|
driver.execute_script("$('[data-field=\"#{name}\"]').editable('setValue', '#{value}');")
|
296
423
|
end
|
297
424
|
end
|
298
|
-
|
425
|
+
# :doc:
|
426
|
+
|
427
|
+
# Fills out large numbers of selectors via a supplied hash in the form id,value_to_send
|
428
|
+
#
|
429
|
+
# ==== Attributes
|
430
|
+
#
|
431
|
+
# * +hash+ - A hash of input IDs associated with the value to send to the input
|
432
|
+
#
|
433
|
+
# ==== Examples
|
434
|
+
#
|
435
|
+
# Fill out a login form
|
436
|
+
# driver.form_fillout({"age"=>"21","num_of_pets"=>"2","num_of_stars"=>"5"})
|
299
437
|
def form_fillout_selector(hash)
|
300
438
|
hash.each do |id,value|
|
301
439
|
option = Selenium::WebDriver::Support::Select.new(driver.find_element(:id => id))
|
@@ -303,7 +441,13 @@ class TelluriumDriver
|
|
303
441
|
end
|
304
442
|
|
305
443
|
end
|
306
|
-
|
444
|
+
|
445
|
+
# Waits for the element specified by the identifiers then clicks where it should be
|
446
|
+
#
|
447
|
+
# ==== Attributes
|
448
|
+
#
|
449
|
+
# * +sym+ - :id, :css, or :name, etc
|
450
|
+
# * +id+ - The text corresponding with the symbol.
|
307
451
|
def wait_and_hover_click(sym,id)
|
308
452
|
found = false
|
309
453
|
#wait until an element with sym,id is displayed. When it is, hover click it
|
@@ -321,6 +465,7 @@ class TelluriumDriver
|
|
321
465
|
self.hover_click(@element)
|
322
466
|
end
|
323
467
|
|
468
|
+
# Closes the webdriver
|
324
469
|
def close
|
325
470
|
driver.quit
|
326
471
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tellurium_driver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Prince
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Provides useful extra methods for Selenium WebDriver, especially helpful
|
14
14
|
in javascript webapps
|