vapir-firefox 1.8.1 → 1.9.0

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.
@@ -31,8 +31,8 @@ module Vapir
31
31
  end
32
32
 
33
33
  def exists?
34
- # jssh_socket may be nil if the window has closed
35
- @modal_window && @browser.jssh_socket && @browser.jssh_socket.object('getWindows()').to_js_array.include(@modal_window)
34
+ # firefox_socket may be nil if the window has closed
35
+ @modal_window && @browser.firefox_socket && @browser.class.window_objects.any?{|window_object| window_object==@modal_window }
36
36
  end
37
37
 
38
38
  def text
@@ -107,7 +107,7 @@ module Vapir
107
107
 
108
108
  def initialize(containing_modal_dialog, options={})
109
109
  options=handle_options(options, :timeout => ModalDialog::DEFAULT_TIMEOUT, :error => true)
110
- @jssh_socket=containing_modal_dialog.browser.jssh_socket
110
+ @firefox_socket=containing_modal_dialog.browser.firefox_socket
111
111
  @browser_object=containing_modal_dialog.modal_window.getBrowser
112
112
 
113
113
  @containing_modal_dialog=containing_modal_dialog
@@ -139,6 +139,6 @@ module Vapir
139
139
  end
140
140
  end
141
141
 
142
- attr_reader :jssh_socket
142
+ attr_reader :firefox_socket
143
143
  end
144
144
  end
@@ -36,24 +36,24 @@ module Vapir
36
36
  # their methods from the top-level context, you get an exception:
37
37
  #
38
38
  # >> browser.element(:tag_name => 'embed').element_object.PercentLoaded()
39
- # JsshError::Error: NPMethod called on non-NPObject wrapped JSObject!
39
+ # FirefoxSocketJavascriptError: NPMethod called on non-NPObject wrapped JSObject!
40
40
  #
41
41
  # but, this method executes script in the context of the content window, so the following works:
42
42
  #
43
43
  # >> browser.execute_script('element.PercentLoaded()', :element => browser.element(:tag_name => 'embed').element_object)
44
44
  # => 100
45
45
  def execute_script(javascript, other_variables={})
46
- sandbox=jssh_socket.Components.utils.Sandbox(content_window_object)
46
+ sandbox=firefox_socket.Components.utils.Sandbox(content_window_object)
47
47
  sandbox.window=content_window_object.window
48
48
  other_variables.each do |name, var|
49
49
  sandbox[name]=var
50
50
  end
51
- return jssh_socket.Components.utils.evalInSandbox('with(window) { '+javascript+' }', sandbox)
51
+ return firefox_socket.Components.utils.evalInSandbox('with(window) { '+javascript+' }', sandbox)
52
52
  end
53
53
 
54
54
  # Returns the html of the document
55
55
  def outer_html
56
- jssh_socket.call_function(:document => document_object) do %Q(
56
+ firefox_socket.call_function(:document => document_object) do %Q(
57
57
  var temp_el=document.createElement('div');
58
58
  for(var i in document.childNodes)
59
59
  { try
@@ -1,5 +1,5 @@
1
1
  module Vapir
2
2
  class Firefox
3
- VERSION = '1.8.1'
3
+ VERSION = '1.9.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vapir-firefox
3
3
  version: !ruby/object:Gem::Version
4
- hash: 53
4
+ hash: 51
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
- - 8
9
- - 1
10
- version: 1.8.1
8
+ - 9
9
+ - 0
10
+ version: 1.9.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ethan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-17 00:00:00 -04:00
18
+ date: 2011-08-04 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -26,12 +26,12 @@ dependencies:
26
26
  requirements:
27
27
  - - "="
28
28
  - !ruby/object:Gem::Version
29
- hash: 53
29
+ hash: 51
30
30
  segments:
31
31
  - 1
32
- - 8
33
- - 1
34
- version: 1.8.1
32
+ - 9
33
+ - 0
34
+ version: 1.9.0
35
35
  type: :runtime
36
36
  version_requirements: *id001
37
37
  - !ruby/object:Gem::Dependency
@@ -48,7 +48,7 @@ dependencies:
48
48
  version: "0"
49
49
  type: :runtime
50
50
  version_requirements: *id002
51
- description: " Vapir-Firefox is a library to programatically drive the Firefox\n browser over the JSSH Firefox extension, exposing a simple-to-use \n and powerful API to make automated testing a simple and joyous affair. \n Forked from the Watir library. \n"
51
+ description: " Vapir-Firefox is a library to programatically drive the Firefox\n browser, exposing a simple-to-use and powerful API to make automated \n testing a simple and joyous affair. \n Forked from the Watir library. \n"
52
52
  email: vapir@googlegroups.com
53
53
  executables: []
54
54
 
@@ -89,8 +89,11 @@ files:
89
89
  - lib/vapir-firefox/elements/text_field.rb
90
90
  - lib/vapir-firefox/elements.rb
91
91
  - lib/vapir-firefox/clear_tracks.rb
92
- - lib/vapir-firefox/jssh_socket.rb
93
- - lib/vapir-firefox/prototype.functional.js
92
+ - lib/vapir-firefox/firefox_socket/base.rb
93
+ - lib/vapir-firefox/firefox_socket/jssh.rb
94
+ - lib/vapir-firefox/firefox_socket/mozrepl.rb
95
+ - lib/vapir-firefox/firefox_socket/prototype.functional.js
96
+ - lib/vapir-firefox/javascript_object.rb
94
97
  has_rdoc: true
95
98
  homepage: http://www.vapir.org/
96
99
  licenses: []
@@ -124,7 +127,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
127
  - 0
125
128
  version: "0"
126
129
  requirements:
127
- - Firefox browser with JSSH extension installed
130
+ - Firefox browser with MozRepl or JSSH extension installed
128
131
  rubyforge_project:
129
132
  rubygems_version: 1.3.7
130
133
  signing_key:
@@ -1,1418 +0,0 @@
1
- require 'json'
2
- require 'socket'
3
- require 'timeout'
4
- #require 'logger'
5
-
6
- # :stopdoc:
7
- #class LoggerWithCallstack < Logger
8
- # class TimeElapsedFormatter < Formatter
9
- # def initialize
10
- # super
11
- # @time_started=Time.now
12
- # end
13
- # def format_datetime(time)
14
- # "%10.3f"%(time.to_f-@time_started.to_f)
15
- # end
16
- #
17
- # end
18
- # def add(severity, message = nil, progname = nil, &block)
19
- # severity ||= UNKNOWN
20
- # if @logdev.nil? or severity < @level
21
- # return true
22
- # end
23
- # progname ||= @progname
24
- # if message.nil?
25
- # if block_given?
26
- # message = yield
27
- # else
28
- # message = progname
29
- # progname = @progname
30
- # end
31
- # end
32
- # message=message.to_s+" FROM: "+caller.map{|c|"\t\t#{c}\n"}.join("")
33
- # @logdev.write(
34
- # format_message(format_severity(severity), Time.now, progname, message))
35
- # true
36
- # end
37
- #end
38
-
39
- # :startdoc:
40
-
41
- # base exception class for all exceptions raised from Jssh sockets and objects.
42
- class JsshError < StandardError;end
43
- # this exception covers all connection errors either on startup or during usage. often it represents an Errno error such as Errno::ECONNRESET.
44
- class JsshConnectionError < JsshError;end
45
- # This exception is thrown if we are unable to connect to JSSh.
46
- class JsshUnableToStart < JsshConnectionError;end
47
- # Represents an error encountered on the javascript side, caught in a try/catch block.
48
- class JsshJavascriptError < JsshError
49
- attr_accessor :source, :js_err, :lineNumber, :stack, :fileName
50
- end
51
- # represents a syntax error in javascript.
52
- class JsshSyntaxError < JsshJavascriptError;end
53
- # raised when a javascript value is expected to be defined but is undefined
54
- class JsshUndefinedValueError < JsshJavascriptError;end
55
-
56
- # A JsshSocket represents a connection to Firefox over a socket opened to the JSSH extension. It
57
- # does the work of interacting with the socket and translating ruby values to javascript and back.
58
- class JsshSocket
59
- # :stopdoc:
60
- # def self.logger
61
- # @@logger||=begin
62
- # logfile=File.open('c:/tmp/jssh_log.txt', File::WRONLY|File::TRUNC|File::CREAT)
63
- # logfile.sync=true
64
- # logger=Logger.new(logfile)
65
- # logger.level = -1#Logger::DEBUG#Logger::INFO
66
- # #logger.formatter=LoggerWithCallstack::TimeElapsedFormatter.new
67
- # logger
68
- # end
69
- # end
70
- # def logger
71
- # self.class.logger
72
- # end
73
-
74
- PROMPT="\n> "
75
-
76
- PrototypeFile=File.join(File.dirname(__FILE__), "prototype.functional.js")
77
- # :startdoc:
78
-
79
- # default IP Address of the machine where the script is to be executed. Default to localhost.
80
- DEFAULT_IP = "127.0.0.1"
81
- # default port to connect to.
82
- DEFAULT_PORT = 9997
83
-
84
- # maximum time JsshSocket waits for a value to be sent before giving up
85
- DEFAULT_SOCKET_TIMEOUT=64
86
- # maximum time JsshSocket will wait for additional reads on a socket that is actively sending
87
- SHORT_SOCKET_TIMEOUT=(2**-2).to_f
88
- # the number of bytes to read from the socket at a time
89
- READ_SIZE=4096
90
-
91
- # the IP to which this socket is connected
92
- attr_reader :ip
93
- # the port on which this socket is connected
94
- attr_reader :port
95
- # whether the prototye javascript library is loaded
96
- attr_reader :prototype
97
-
98
- # Connects a new socket to jssh
99
- #
100
- # Takes options:
101
- # * :jssh_ip => the ip to connect to, default 127.0.0.1
102
- # * :jssh_port => the port to connect to, default 9997
103
- # * :send_prototype => true|false, whether to load and send the Prototype library (the functional programming part of it anyway, and JSON bits)
104
- def initialize(options={})
105
- @ip=options[:jssh_ip] || DEFAULT_IP
106
- @port=options[:jssh_port] || DEFAULT_PORT
107
- @prototype=options.key?(:send_prototype) ? options[:send_prototype] : true
108
- begin
109
- @socket = TCPSocket::new(@ip, @port)
110
- @socket.sync = true
111
- @expecting_prompt=false # initially, the welcome message comes before the prompt, so this so this is false to start with
112
- @expecting_extra_maybe=false
113
- welcome="Welcome to the Mozilla JavaScript Shell!\n"
114
- read=read_value
115
- if !read
116
- @expecting_extra_maybe=true
117
- raise JsshUnableToStart, "Something went wrong initializing - no response"
118
- elsif read != welcome
119
- @expecting_extra_maybe=true
120
- raise JsshUnableToStart, "Something went wrong initializing - message #{read.inspect} != #{welcome.inspect}"
121
- end
122
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
123
- err=JsshUnableToStart.new("Could not connect to JSSH sever #{@ip}:#{@port}. Ensure that Firefox is running and has JSSH configured, or try restarting firefox.\nMessage from TCPSocket:\n#{$!.message}")
124
- err.set_backtrace($!.backtrace)
125
- raise err
126
- end
127
- if @prototype
128
- ret=send_and_read(File.read(PrototypeFile))
129
- if ret != "done!"
130
- @expecting_extra_maybe=true
131
- raise JsshError, "Something went wrong loading Prototype - message #{ret.inspect}"
132
- end
133
- end
134
- ret=send_and_read("(function()
135
- { nativeJSON=Components.classes['@mozilla.org/dom/json;1'].createInstance(Components.interfaces.nsIJSON);
136
- nativeJSON_encode_length=function(object)
137
- { var encoded=nativeJSON.encode(object);
138
- return encoded.length.toString()+\"\\n\"+encoded;
139
- }
140
- return 'done!';
141
- })()")
142
- if ret != "done!"
143
- @expecting_extra_maybe=true
144
- raise JsshError, "Something went wrong initializing native JSON - message #{ret.inspect}"
145
- end
146
- root.JsshTemp={}
147
- end
148
-
149
- private
150
- # sets the error state if an exception is encountered while running the given block. the
151
- # exception is not rescued.
152
- def ensuring_extra_handled
153
- begin
154
- yield
155
- rescue Exception
156
- @expecting_extra_maybe = true
157
- raise
158
- end
159
- end
160
- # reads from the socket and returns what seems to be the value that should be returned, by stripping prompts
161
- # from the beginning and end where appropriate.
162
- #
163
- # does not deal with prompts in between values, because attempting to parse those out is impossible, it being
164
- # perfectly possible that a string the same as the prompt is part of actual data. (even stripping it from the
165
- # ends of the string is not entirely certain; data could have it at the ends too, but that's the best that can
166
- # be done.) so, read_value should be called after every line, or you can end up with stuff like:
167
- #
168
- # >> @socket.send "3\n4\n5\n", 0
169
- # => 6
170
- # >> read_value
171
- # => "3\n> 4\n> 5"
172
- #
173
- # by default, read_value reads until the socket is done being ready. "done being ready" is defined as Kernel.select
174
- # saying that the socket isn't ready after waiting for SHORT_SOCKET_TIMEOUT. usually this will be true after a
175
- # single read, as most things only take one #recv call to get the whole value. this waiting for SHORT_SOCKET_TIMEOUT
176
- # can add up to being slow if you're doing a lot of socket activity.
177
- #
178
- # to solve this, performance can be improved significantly using the :length_before_value option. with this, you have
179
- # to write your javascript to return the length of the value to be sent, followed by a newline, followed by the actual
180
- # value (which must be of the length it says it is, or this method will error).
181
- #
182
- # if this option is set, this doesn't do any SHORT_SOCKET_TIMEOUT waiting once it gets the full value, it returns
183
- # immediately.
184
- def read_value(options={})
185
- options={:timeout => DEFAULT_SOCKET_TIMEOUT, :length_before_value => false, :read_size => READ_SIZE}.merge(options)
186
- received_data = []
187
- value_string = ""
188
- size_to_read=options[:read_size]
189
- timeout=options[:timeout]
190
- already_read_length=false
191
- expected_size=nil
192
- # logger.add(-1) { "RECV_SOCKET is starting. timeout=#{timeout}" }
193
- while size_to_read > 0 && ensuring_extra_handled { Kernel.select([@socket] , nil , nil, timeout) }
194
- data = ensuring_extra_handled { @socket.recv(size_to_read) }
195
- received_data << data
196
- value_string << data
197
- if @expecting_prompt && utf8_length_safe(value_string) > PROMPT.length
198
- if value_string =~ /\A#{Regexp.escape(PROMPT)}/
199
- value_string.sub!(/\A#{Regexp.escape(PROMPT)}/, '')
200
- @expecting_prompt=false
201
- else
202
- value_string << clear_error
203
- raise JsshError, "Expected a prompt! received unexpected data #{value_string.inspect}. maybe left on the socket by last evaluated expression? last expression was:\n\n#{@last_expression}"
204
- end
205
- end
206
- if !@expecting_prompt
207
- if options[:length_before_value] && !already_read_length && value_string.length > 0
208
- if value_string =~ /\A(\d+)\n/
209
- expected_size=$1.to_i
210
- already_read_length=true
211
- value_string.sub!(/\A\d+\n/, '')
212
- elsif value_string =~ /\A\d+\z/
213
- # rather unlikely, but maybe we just received part of the number so far - ignore
214
- else
215
- @expecting_extra_maybe=true
216
- raise JsshError, "Expected length! unexpected data with no preceding length received: #{value_string.inspect}"
217
- end
218
- end
219
- if expected_size
220
- size_to_read = expected_size - utf8_length_safe(value_string)
221
- end
222
- unless value_string.empty? # switch to short timeout - unless we got a prompt (leaving value_string blank). switching to short timeout when all we got was a prompt would probably accidentally leave the value on the socket.
223
- timeout=SHORT_SOCKET_TIMEOUT
224
- end
225
- end
226
-
227
- # Kernel.select seems to indicate that a dead socket is ready to read, and returns endless blank strings to recv. rather irritating.
228
- if received_data.length >= 3 && received_data[-3..-1].all?{|rd| rd==''}
229
- raise JsshConnectionError, "Socket seems to no longer be connected"
230
- end
231
- # logger.add(-1) { "RECV_SOCKET is continuing. timeout=#{timeout}; data=#{data.inspect}" }
232
- end
233
- # logger.debug { "RECV_SOCKET is done. received_data=#{received_data.inspect}; value_string=#{value_string.inspect}" }
234
- if @expecting_extra_maybe
235
- if Kernel.select([@socket] , nil , nil, SHORT_SOCKET_TIMEOUT)
236
- cleared_error=clear_error
237
- if cleared_error==PROMPT
238
- # if all we got was the prompt, just stick it on the value here so that the code below will deal with setting @execting_prompt correctly
239
- value_string << cleared_error
240
- else
241
- raise JsshError, "We finished receiving but the socket was still ready to send! extra data received were: #{cleared_error}"
242
- end
243
- end
244
- @expecting_extra_maybe=false
245
- end
246
-
247
- if expected_size
248
- value_string_length=value_string.unpack("U*").length # JSSH returns a utf-8 string, so unpack each character to get the right length
249
-
250
- if value_string_length == expected_size
251
- @expecting_prompt=true
252
- elsif value_string_length == expected_size + PROMPT.length && value_string =~ /#{Regexp.escape(PROMPT)}\z/
253
- value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
254
- @expecting_prompt=false
255
- else
256
- @expecting_extra_maybe=true if value_string_length < expected_size
257
- raise JsshError, "Expected a value of size #{expected_size}; received data of size #{value_string_length}: #{value_string.inspect}"
258
- end
259
- else
260
- if value_string =~ /#{Regexp.escape(PROMPT)}\z/ # what if the value happens to end with the same string as the prompt?
261
- value_string.sub!(/#{Regexp.escape(PROMPT)}\z/, '')
262
- @expecting_prompt=false
263
- else
264
- @expecting_prompt=true
265
- end
266
- end
267
- return value_string
268
- end
269
-
270
- private
271
- # returns the number of complete utf-8 encoded characters in the string, without erroring on
272
- # partial characters.
273
- def utf8_length_safe(string)
274
- string=string.dup
275
- begin
276
- string.unpack("U*").length
277
- rescue ArgumentError # this happens when the socket receive gets split across a utf8 character. we drop the incomplete character from the end.
278
- if $!.message =~ /malformed UTF-8 character \(expected \d+ bytes, given (\d+) bytes\)/
279
- given=$1.to_i
280
- string[0...(-given)].unpack("U*").length
281
- else # otherwise, this is some other issue we weren't expecting; we will not rescue it.
282
- raise
283
- end
284
- end
285
- end
286
- # this should be called when an error occurs and we want to clear the socket of any value remaining on it.
287
- # tries for SHORT_SOCKET_TIMEOUT to see if a value will appear on the socket; if one does, returns it.
288
- def clear_error
289
- data=""
290
- while Kernel.select([@socket], nil, nil, SHORT_SOCKET_TIMEOUT)
291
- # clear any other crap left on the socket
292
- data << @socket.recv(READ_SIZE)
293
- end
294
- if data =~ /#{Regexp.escape(PROMPT)}\z/
295
- @expecting_prompt=false
296
- end
297
- data
298
- end
299
-
300
- # sends the given javascript expression which is evaluated, reads the resulting value from the socket, and returns that value.
301
- #
302
- # options are passed to #read_value untouched; the only one that probably ought to be used here is :timeout.
303
- def send_and_read(js_expr, options={})
304
- # logger.add(-1) { "SEND_AND_READ is starting. options=#{options.inspect}" }
305
- @last_expression=js_expr
306
- js_expr=js_expr+"\n" unless js_expr =~ /\n\z/
307
- # logger.debug { "SEND_AND_READ sending #{js_expr.inspect}" }
308
- @socket.send(js_expr, 0)
309
- return read_value(options)
310
- end
311
-
312
- private
313
- # creates a ruby exception from the given information and raises it.
314
- def js_error(errclassname, message, source, stuff={})
315
- errclass=if errclassname
316
- unless JsshError.const_defined?(errclassname)
317
- JsshError.const_set(errclassname, Class.new(JsshJavascriptError))
318
- end
319
- JsshError.const_get(errclassname)
320
- else
321
- JsshJavascriptError
322
- end
323
- err=errclass.new("#{message}\nEvaluating:\n#{source}\n\nOther stuff:\n#{stuff.inspect}")
324
- err.source=source
325
- err.js_err=stuff
326
- ["lineNumber", "stack", "fileName"].each do |attr|
327
- if stuff.key?(attr)
328
- err.send("#{attr}=", stuff[attr])
329
- end
330
- end
331
- raise err
332
- end
333
- public
334
-
335
- def self.to_javascript(object)
336
- if ['Array', 'Set'].any?{|klass_name| Object.const_defined?(klass_name) && object.is_a?(Object.const_get(klass_name)) }
337
- "["+object.map{|element| to_javascript(element) }.join(", ")+"]"
338
- elsif object.is_a?(Hash)
339
- "{"+object.map{|(key, value)| to_javascript(key)+": "+to_javascript(value) }.join(", ")+"}"
340
- elsif object.is_a?(JsshObject)
341
- object.ref
342
- elsif [true, false, nil].include?(object) || [Integer, Float, String, Symbol].any?{|klass| object.is_a?(klass) }
343
- object.to_json
344
- elsif object.is_a?(Regexp)
345
- # get the flags javascript recognizes - not the same ones as ruby.
346
- js_flags = {Regexp::MULTILINE => 'm', Regexp::IGNORECASE => 'i'}.inject("") do |flags, (bit, flag)|
347
- flags + (object.options & bit > 0 ? flag : '')
348
- end
349
- # "new RegExp("+to_javascript(object.source)+", "+to_javascript(js_flags)+")"
350
- js_source = object.source.empty? ? "/(?:)/" : object.inspect
351
- js_source.sub!(/\w*\z/, '') # drop ruby flags
352
- js_source + js_flags
353
- else
354
- raise "Unable to represent object as javascript: #{object.inspect} (#{object.class})"
355
- end
356
- end
357
-
358
- # returns the value of the given javascript expression, as reported by JSSH.
359
- #
360
- # This will be a string, the given expression's toString.
361
- def value(js)
362
- # this is wrapped in a function so that ...
363
- # dang, now I can't remember. I'm sure I had a good reason at the time.
364
- send_and_read("(function(){return #{js}})()")
365
- end
366
-
367
- # assigns to the javascript reference on the left the javascript expression on the right.
368
- # returns the value of the expression as reported by JSSH, which
369
- # will be a string, the expression's toString. Uses #value; see its documentation.
370
- def assign(js_left, js_right)
371
- value("#{js_left}= #{js_right}")
372
- end
373
-
374
- # calls to the given function (javascript reference to a function) passing it the
375
- # given arguments (javascript expressions). returns the return value of the function,
376
- # a string, the toString of the javascript value. Uses #value; see its documentation.
377
- def call(js_function, *js_args)
378
- value("#{js_function}(#{js_args.join(', ')})")
379
- end
380
-
381
- # if the given javascript expression ends with an = symbol, #handle calls to #assign
382
- # assuming it is given one argument; if the expression refers to a function, calls
383
- # that function with the given arguments using #call; if the expression is some other
384
- # value, returns that value (its javascript toString), calling #value, assuming
385
- # given no arguments. Uses #value; see its documentation.
386
- def handle(js_expr, *args)
387
- if js_expr=~/=\z/ # doing assignment
388
- js_left=$`
389
- if args.size != 1
390
- raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
391
- end
392
- assign(js_left, *args)
393
- else
394
- type=typeof(js_expr)
395
- case type
396
- when "function"
397
- call(js_expr, *args)
398
- when "undefined"
399
- raise JsshUndefinedValueError, "undefined expression #{js_expr.inspect}"
400
- else
401
- if !args.empty?
402
- raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
403
- end
404
- value(js_expr)
405
- end
406
- end
407
- end
408
-
409
- # returns the value of the given javascript expression. Assuming that it can
410
- # be converted to JSON, will return the equivalent ruby data type to the javascript
411
- # value. Will raise an error if the javascript errors.
412
- def value_json(js, options={})
413
- options={:error_on_undefined => true}.merge(options)
414
- raise ArgumentError, "Expected a string containing a javascript expression! received #{js.inspect} (#{js.class})" unless js.is_a?(String)
415
- ensure_prototype
416
- ref_error=options[:error_on_undefined] ? "typeof(result)=='undefined' ? {errored: true, value: {'name': 'ReferenceError', 'message': 'undefined expression in: '+result_f.toString()}} : " : ""
417
- wrapped_js=
418
- "try
419
- { var result_f=(function(){return #{js}});
420
- var result=result_f();
421
- nativeJSON_encode_length(#{ref_error} {errored: false, value: result});
422
- }catch(e)
423
- { nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
424
- }"
425
- val=send_and_read(wrapped_js, options.merge(:length_before_value => true))
426
- error_or_val_json(val, js)
427
- end
428
- private
429
- # takes a json value (a string) of the form {errored: boolean, value: anything},
430
- # checks if an error is indicated, and creates and raises an appropriate exception
431
- # if so.
432
- def error_or_val_json(val, js)
433
- if !val || val==''
434
- @expecting_extra_maybe=true
435
- raise JsshError, "received no value! may have timed out waiting for a value that was not coming."
436
- end
437
- if val=~ /\ASyntaxError: /
438
- raise JsshSyntaxError, val
439
- end
440
- errord_and_val=parse_json(val)
441
- unless errord_and_val.is_a?(Hash) && errord_and_val.keys.sort == ['errored', 'value'].sort
442
- raise RuntimeError, "unexpected result: \n\t#{errord_and_val.inspect} \nencountered parsing value: \n\t#{val.inspect} \nreturned from expression: \n\t#{js.inspect}"
443
- end
444
- errord=errord_and_val['errored']
445
- val= errord_and_val['value']
446
- if errord
447
- case val
448
- when Hash
449
- js_error(val['name'],val['message'],js,val)
450
- when String
451
- js_error(nil, val, js)
452
- else
453
- js_error(nil, val.inspect, js)
454
- end
455
- else
456
- val
457
- end
458
- end
459
- public
460
-
461
- # assigns to the javascript reference on the left the object on the right.
462
- # Assuming the right object can be converted to JSON, the javascript value will
463
- # be the equivalent javascript data type to the ruby object. Will return
464
- # the assigned value, converted from its javascript value back to ruby. So, the return
465
- # value won't be exactly equivalent if you use symbols for example.
466
- #
467
- # >> jssh_socket.assign_json('bar', {:foo => [:baz, 'qux']})
468
- # => {"foo"=>["baz", "qux"]}
469
- #
470
- # Uses #value_json; see its documentation.
471
- def assign_json(js_left, rb_right)
472
- ensure_prototype
473
- js_right=JsshSocket.to_javascript(rb_right)
474
- value_json("#{js_left}=#{js_right}")
475
- end
476
-
477
- # calls to the given function (javascript reference to a function) passing it the
478
- # given arguments, each argument being converted from a ruby object to a javascript object
479
- # via JSON. returns the return value of the function, of equivalent type to the javascript
480
- # return value, converted from javascript to ruby via JSON.
481
- # Uses #value_json; see its documentation.
482
- def call_json(js_function, *rb_args)
483
- ensure_prototype
484
- js_args=rb_args.map{|arg| JsshSocket.to_javascript(arg) }
485
- value_json("#{js_function}(#{js_args.join(', ')})")
486
- end
487
-
488
- # does the same thing as #handle, but with json, calling #assign_json, #value_json,
489
- # or #call_json.
490
- #
491
- # if the given javascript expression ends with an = symbol, #handle_json calls to
492
- # #assign_json assuming it is given one argument; if the expression refers to a function,
493
- # calls that function with the given arguments using #call_json; if the expression is
494
- # some other value, returns that value, converted to ruby via JSON, assuming given no
495
- # arguments. Uses #value_json; see its documentation.
496
- def handle_json(js_expr, *args)
497
- ensure_prototype
498
- if js_expr=~/=\z/ # doing assignment
499
- js_left=$`
500
- if args.size != 1
501
- raise ArgumentError, "Assignment (#{js_expr}) must take one argument"
502
- end
503
- assign_json(js_left, *args)
504
- else
505
- type=typeof(js_expr)
506
- case type
507
- when "function"
508
- call_json(js_expr, *args)
509
- when "undefined"
510
- raise JsshUndefinedValueError, "undefined expression #{js_expr}"
511
- else
512
- if !args.empty?
513
- raise ArgumentError, "Cannot pass arguments to expression #{js_expr.inspect} of type #{type}"
514
- end
515
- value_json(js_expr)
516
- end
517
- end
518
- end
519
-
520
- # raises error if the prototype library (needed for JSON stuff in javascript) has not been loaded
521
- def ensure_prototype
522
- unless prototype
523
- raise JsshError, "This functionality requires the prototype library; cannot be called on a Jssh session that has not loaded the Prototype library"
524
- end
525
- end
526
-
527
- # returns the type of the given expression using javascript typeof operator, with the exception that
528
- # if the expression is null, returns 'null' - whereas typeof(null) in javascript returns 'object'
529
- def typeof(expression)
530
- ensure_prototype
531
- js="try
532
- { nativeJSON_encode_length({errored: false, value: (function(object){ return (object===null) ? 'null' : (typeof object); })(#{expression})});
533
- } catch(e)
534
- { if(e.name=='ReferenceError')
535
- { nativeJSON_encode_length({errored: false, value: 'undefined'});
536
- }
537
- else
538
- { nativeJSON_encode_length({errored: true, value: Object.extend({}, e)});
539
- }
540
- }"
541
- error_or_val_json(send_and_read(js, :length_before_value => true),js)
542
- end
543
-
544
- # uses the javascript 'instanceof' operator, passing it the given
545
- # expression and interface. this should return true or false.
546
- def instanceof(js_expression, js_interface)
547
- value_json "(#{js_expression}) instanceof (#{js_interface})"
548
- end
549
-
550
- # parses the given JSON string using JSON.parse
551
- # Raises JSON::ParserError if given a blank string, something that is not a string, or
552
- # a string that contains invalid JSON
553
- def parse_json(json)
554
- err_class=JSON::ParserError
555
- decoder=JSON.method(:parse)
556
- # err_class=ActiveSupport::JSON::ParseError
557
- # decoder=ActiveSupport::JSON.method(:decode)
558
- raise err_class, "Not a string! got: #{json.inspect}" unless json.is_a?(String)
559
- raise err_class, "Blank string!" if json==''
560
- begin
561
- return decoder.call(json)
562
- rescue err_class
563
- err=$!.class.new($!.message+"\nParsing: #{json.inspect}")
564
- err.set_backtrace($!.backtrace)
565
- raise err
566
- end
567
- end
568
-
569
- # takes a reference and returns a new JsshObject representing that reference on this socket.
570
- # ref should be a string representing a reference in javascript.
571
- def object(ref, other={})
572
- JsshObject.new(ref, self, {:debug_name => ref}.merge(other))
573
- end
574
- # takes a reference and returns a new JsshObject representing that reference on this socket,
575
- # stored on this socket's temporary object.
576
- def object_in_temp(ref, other={})
577
- object(ref, other).store_rand_temp
578
- end
579
-
580
- # represents the root of the space seen by the JsshSocket, and implements #method_missing to
581
- # return objects at the root level in a similar manner to JsshObject's #method_missing.
582
- #
583
- # for example, jssh_socket.root.Components will return the top-level Components object;
584
- # jssh_socket.root.ctypes will return the ctypes top-level object if that is defined, or error
585
- # if not.
586
- #
587
- # if the object is a function, then it will be called with any given arguments:
588
- # >> jssh_socket.root.getWindows
589
- # => #<JsshObject:0x0254d150 type=object, debug_name=getWindows()>
590
- # >> jssh_socket.root.eval("3+2")
591
- # => 5
592
- #
593
- # If any arguments are given to an object that is not a function, you will get an error:
594
- # >> jssh_socket.root.Components('wat')
595
- # ArgumentError: Cannot pass arguments to Javascript object #<JsshObject:0x02545978 type=object, debug_name=Components>
596
- #
597
- # special behaviors exist for the suffixes !, ?, and =.
598
- #
599
- # - '?' suffix returns nil if the object does not exist, rather than raising an exception. for
600
- # example:
601
- # >> jssh_socket.root.foo
602
- # JsshUndefinedValueError: undefined expression represented by #<JsshObject:0x024c3ae0 type=undefined, debug_name=foo> (javascript reference is foo)
603
- # >> jssh_socket.root.foo?
604
- # => nil
605
- # - '=' suffix sets the named object to what is given, for example:
606
- # >> jssh_socket.root.foo?
607
- # => nil
608
- # >> jssh_socket.root.foo={:x => ['y', 'z']}
609
- # => {:x=>["y", "z"]}
610
- # >> jssh_socket.root.foo
611
- # => #<JsshObject:0x024a3510 type=object, debug_name=foo>
612
- # - '!' suffix tries to convert the value to json in javascrit and back from json to ruby, even
613
- # when it might be unsafe (causing infinite rucursion or other errors). for example:
614
- # >> jssh_socket.root.foo!
615
- # => {"x"=>["y", "z"]}
616
- # it can be used with function results that would normally result in a JsshObject:
617
- # >> jssh_socket.root.eval!("[1, 2, 3]")
618
- # => [1, 2, 3]
619
- # and of course it can error if you try to do something you shouldn't:
620
- # >> jssh_socket.root.getWindows!
621
- # JsshError::NS_ERROR_FAILURE: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIJSON.encode]
622
- def root
623
- jssh_socket=self
624
- # @root ||= begin
625
- root = Object.new
626
- (class << root; self; end).send(:define_method, :method_missing) do |method, *args|
627
- method=method.to_s
628
- if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
629
- method = $1
630
- suffix = $2
631
- jssh_socket.object(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
632
- else
633
- # don't deal with any special character crap
634
- super
635
- end
636
- end
637
- root
638
- # end
639
- end
640
-
641
- # Creates and returns a JsshObject representing a function.
642
- #
643
- # Takes any number of arguments, which should be strings or symbols, which are arguments to the
644
- # javascript function.
645
- #
646
- # The javascript function is specified as the result of a block which must be given to
647
- # #function.
648
- #
649
- # An example:
650
- # jssh_socket.function(:a, :b) do
651
- # "return a+b;"
652
- # end
653
- # => #<JsshObject:0x0248e78c type=function, debug_name=function(a, b){ return a+b; }>
654
- #
655
- # This is exactly the same as doing
656
- # jssh_socket.object("function(a, b){ return a+b; }")
657
- # but it is a bit more concise and reads a bit more ruby-like.
658
- #
659
- # a longer example to return the text of a thing (rather contrived, but, it works):
660
- #
661
- # jssh_socket.function(:node) do %q[
662
- # if(node.nodeType==3)
663
- # { return node.data;
664
- # }
665
- # else if(node.nodeType==1)
666
- # { return node.textContent;
667
- # }
668
- # else
669
- # { return "what?";
670
- # }
671
- # ]
672
- # end.call(some_node)
673
- def function(*arg_names)
674
- unless arg_names.all?{|arg| (arg.is_a?(String) || arg.is_a?(Symbol)) && arg.to_s =~ /\A[a-z_][a-z0-9_]*\z/i }
675
- raise ArgumentError, "Arguments to \#function should be strings or symbols representing the names of arguments to the function. got #{arg_names.inspect}"
676
- end
677
- unless block_given?
678
- raise ArgumentError, "\#function should be given a block which results in a string representing the body of a javascript function. no block was given!"
679
- end
680
- function_body = yield
681
- unless function_body.is_a?(String)
682
- raise ArgumentError, "The block given to \#function must return a string representing the body of a javascript function! instead got #{function_body.inspect}"
683
- end
684
- nl = function_body.include?("\n") ? "\n" : ""
685
- object("function(#{arg_names.join(", ")})#{nl}{ #{function_body} #{nl}}")
686
- end
687
-
688
- # takes a hash of arguments with keys that are strings or symbols that will be variables in the
689
- # scope of the function in javascript, and a block which results in a string which should be the
690
- # body of a javascript function. calls the given function with the given arguments.
691
- #
692
- # an example:
693
- # jssh_socket.call_function(:x => 3, :y => {:z => 'foobar'}) do
694
- # "return x + y['z'].length;"
695
- # end
696
- #
697
- # will return 9.
698
- def call_function(arguments_hash={}, &block)
699
- argument_names, argument_vals = *arguments_hash.inject([[],[]]) do |(names, vals),(name, val)|
700
- [names + [name], vals + [val]]
701
- end
702
- function(*argument_names, &block).call(*argument_vals)
703
- end
704
-
705
- # returns a JsshObject representing a designated top-level object for temporary storage of stuff
706
- # on this socket.
707
- #
708
- # really, temporary values could be stored anywhere. this just gives one nice consistent designated place to stick them.
709
- def temp_object
710
- @temp_object ||= root.JsshTemp
711
- end
712
- # returns a JsshObject representing the Components top-level javascript object.
713
- #
714
- # https://developer.mozilla.org/en/Components_object
715
- def Components
716
- @components ||= root.Components
717
- end
718
- # returns a JsshObject representing the return value of JSSH's builtin getWindows() function.
719
- def getWindows
720
- root.getWindows
721
- end
722
- # raises an informative error if the socket is down for some reason
723
- def assert_socket
724
- begin
725
- actual, expected=if prototype
726
- [value_json('["foo"]'), ["foo"]]
727
- else
728
- [value('"foo"'), "foo"]
729
- end
730
- rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE
731
- raise(JsshConnectionError, "Encountered a socket error while checking the socket.\n#{$!.class}\n#{$!.message}", $!.backtrace)
732
- end
733
- unless expected==actual
734
- raise JsshError, "The socket seems to have a problem: sent #{expected.inspect} but got back #{actual.inspect}"
735
- end
736
- end
737
-
738
- # returns a string of basic information about this socket.
739
- def inspect
740
- "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:ip, :port, :prototype].map{|attr| aa="@#{attr}";aa+'='+instance_variable_get(aa).inspect}.join(', ')}>"
741
- end
742
- end
743
-
744
- # represents a javascript object in ruby.
745
- class JsshObject
746
- # the reference to the javascript object this JsshObject represents
747
- attr_reader :ref
748
- # the JsshSocket this JsshObject is on
749
- attr_reader :jssh_socket
750
- # whether this represents the result of a function call (if it does, then JsshSocket#typeof won't be called on it)
751
- attr_reader :function_result
752
- # this tracks the origins of this object - what calls were made along the way to get it.
753
- attr_reader :debug_name
754
- # :stopdoc:
755
- # def logger
756
- # jssh_socket.logger
757
- # end
758
-
759
- # :startdoc:
760
-
761
- public
762
- # initializes a JsshObject with a string of javascript containing a reference to
763
- # the object, and a JsshSocket that the object is defined on.
764
- def initialize(ref, jssh_socket, other={})
765
- other={:debug_name => ref, :function_result => false}.merge(other)
766
- raise ArgumentError, "Empty object reference!" if !ref || ref==''
767
- raise ArgumentError, "Reference must be a string - got #{ref.inspect} (#{ref.class.name})" unless ref.is_a?(String)
768
- raise ArgumentError, "Not given a JsshSocket, instead given #{jssh_socket.inspect} (#{jssh_socket.class.name})" unless jssh_socket.is_a?(JsshSocket)
769
- @ref=ref
770
- @jssh_socket=jssh_socket
771
- @debug_name=other[:debug_name]
772
- @function_result=other[:function_result]
773
- # logger.info { "#{self.class} initialized: #{debug_name} (type #{type})" }
774
- end
775
-
776
- # returns the value, via JsshSocket#value_json
777
- def val
778
- jssh_socket.value_json(ref, :error_on_undefined => !function_result)
779
- end
780
-
781
- # whether JsshObject shall try to dynamically define methods on initialization, using
782
- # #define_methods! default is false.
783
- def self.always_define_methods
784
- unless class_variable_defined?('@@always_define_methods')
785
- # if not defined, set the default.
786
- @@always_define_methods=false
787
- end
788
- @@always_define_methods
789
- end
790
- # set whether JsshObject shall try to dynamically define methods in #val_or_object, using
791
- # #define_methods!
792
- #
793
- # I find this useful to set to true in irb, for tab-completion of methods. it may cause
794
- # jssh operations to be considerably slower, however.
795
- #
796
- # for always setting this in irb, I set this beforehand, overriding the default,
797
- # by including in my .irbrc the following (which doesn't require jssh_socket.rb to be
798
- # required):
799
- #
800
- # class JsshObject
801
- # @@always_define_methods=true
802
- # end
803
- def self.always_define_methods=(val)
804
- @@always_define_methods = val
805
- end
806
-
807
- # returns the value just as a string with no attempt to deal with type using json. via JsshSocket#value
808
- #
809
- # note that this can be slow if it evaluates to a blank string. for example, if ref is just ""
810
- # then JsshSocket#value will wait DEFAULT_SOCKET_TIMEOUT seconds for data that is not to come.
811
- # this also happens with functions that return undefined. if ref="function(){do_some_stuff;}"
812
- # (with no return), it will also wait DEFAULT_SOCKET_TIMEOUT.
813
- def val_str
814
- jssh_socket.value(ref)
815
- end
816
-
817
- # returns javascript typeof this object
818
- def type
819
- if function_result # don't get type for function results, causes function evaluations when you probably didn't want that.
820
- nil
821
- else
822
- # logger.add(-1) { "retrieving type for #{debug_name}" }
823
- @type||= jssh_socket.typeof(ref)
824
- end
825
- end
826
-
827
- # calls the javascript instanceof operator on this object and the given interface (expected to
828
- # be a JsshObject) note that the javascript instanceof operator is not to be confused with
829
- # ruby's #instance_of? method - this takes a javascript interface; #instance_of? takes a ruby
830
- # module.
831
- #
832
- # example:
833
- # window.instanceof(window.jssh_socket.Components.interfaces.nsIDOMChromeWindow)
834
- # => true
835
- def instanceof(interface)
836
- jssh_socket.instanceof(self.ref, interface.ref)
837
- end
838
- # returns an array of interfaces which this object is an instance of. this is achieved
839
- # by looping over each value of Components.interfaces (see https://developer.mozilla.org/en/Components.interfaces )
840
- # and calling the #instanceof operator with this and the interface.
841
- #
842
- # this may be rather slow.
843
- def implemented_interfaces
844
- jssh_socket.Components.interfaces.to_hash.inject([]) do |list, (key, interface)|
845
- list << interface if instanceof(interface)
846
- list
847
- end
848
- end
849
-
850
- # returns the type of object that is reported by the javascript toString() method, which
851
- # returns such as "[object Object]" or "[object XPCNativeWrapper [object HTMLDocument]]"
852
- # This method returns 'Object' or 'XPCNativeWrapper [object HTMLDocument]' respectively.
853
- # Raises an error if this JsshObject points to something other than a javascript 'object'
854
- # type ('function' or 'number' or whatever)
855
- #
856
- # this isn't used, doesn't seem useful, and may go away in the future.
857
- def object_type
858
- @object_type ||= begin
859
- case type
860
- when 'object'
861
- self.toString! =~ /\A\[object\s+(.*)\]\Z/
862
- $1
863
- else
864
- raise JsshError, "Type is #{type}, not object"
865
- end
866
- end
867
- end
868
-
869
- # checks the type of this object, and if it is a type that can be simply converted to a ruby
870
- # object via json, returns the ruby value. that occurs if the type is one of:
871
- #
872
- # 'boolean','number','string','null'
873
- #
874
- # otherwise - if the type is something else (probably 'function' or 'object'; or maybe something else)
875
- # then this JsshObject is returned.
876
- #
877
- # if the object this refers to is undefined in javascript, then behavor depends on the options
878
- # hash. if :error_on_undefined is true, then nil is returned; otherwise JsshUndefinedValueError
879
- # is raised.
880
- #
881
- # if this is a function result, this will store the result in a temporary location (thereby
882
- # calling the function to acquire the result) before making the above decision.
883
- #
884
- # this method also calls #define_methods! on this if JsshObject.always_define_methods is true.
885
- # this can be overridden in the options hash using the :define_methods key (true or false).
886
- def val_or_object(options={})
887
- options={:error_on_undefined=>true, :define_methods => self.class.always_define_methods}.merge(options)
888
- if function_result # calling functions multiple times is bad, so store in temp before figuring out what to do with it
889
- store_rand_object_key(jssh_socket.temp_object).val_or_object(options.merge(:error_on_undefined => false))
890
- else
891
- case self.type
892
- when 'undefined'
893
- if !options[:error_on_undefined]
894
- nil
895
- else
896
- raise JsshUndefinedValueError, "undefined expression represented by #{self.inspect} (javascript reference is #{@ref})"
897
- end
898
- when 'boolean','number','string','null'
899
- val
900
- else # 'function','object', or anything else
901
- if options[:define_methods] && type=='object'
902
- define_methods!
903
- end
904
- self
905
- end
906
- end
907
- end
908
- # does the work of #method_missing to determine whether to call a function what to return based
909
- # on the defined behavior of the given suffix. see #method_missing for more. information.
910
- def assign_or_call_or_val_or_object_by_suffix(suffix, *args)
911
- if suffix=='='
912
- assign(*args)
913
- else
914
- obj = if type=='function'
915
- pass(*args)
916
- elsif !args.empty?
917
- raise ArgumentError, "Cannot pass arguments to Javascript object #{inspect} (ref = #{ref})"
918
- else
919
- self
920
- end
921
- case suffix
922
- when nil
923
- obj.val_or_object
924
- when '?'
925
- obj.val_or_object(:error_on_undefined => false)
926
- when '!'
927
- obj.val
928
- else
929
- raise ArgumentError, "suffix should be one of: nil, '?', '!', '='; got: #{suffix.inspect}"
930
- end
931
- end
932
- end
933
-
934
- # returns a JsshObject representing the given attribute. Checks the type, and if it is a
935
- # function, calls the function with any arguments given (which are converted to javascript)
936
- # and returns the return value of the function (or nil if the function returns undefined).
937
- #
938
- # If the attribute is undefined, raises an error (if you want an attribute even if it's
939
- # undefined, use #invoke? or #attr).
940
- def invoke(attribute, *args)
941
- attr(attribute).assign_or_call_or_val_or_object_by_suffix(nil, *args)
942
- end
943
- # same as #invoke, but returns nil for undefined attributes rather than raising an
944
- # error.
945
- def invoke?(attribute, *args)
946
- attr(attribute).assign_or_call_or_val_or_object_by_suffix('?', *args)
947
- end
948
-
949
- # returns a JsshObject referencing the given attribute of this object
950
- def attr(attribute, options={})
951
- unless (attribute.is_a?(String) || attribute.is_a?(Symbol)) && attribute.to_s =~ /\A[a-z_][a-z0-9_]*\z/i
952
- raise JsshSyntaxError, "#{attribute.inspect} (#{attribute.class.inspect}) is not a valid attribute!"
953
- end
954
- JsshObject.new("#{ref}.#{attribute}", jssh_socket, :debug_name => "#{debug_name}.#{attribute}")
955
- end
956
-
957
- # assigns the given ruby value (converted to javascript) to the reference
958
- # for this object. returns self.
959
- def assign(val)
960
- @debug_name="(#{debug_name}=#{val.is_a?(JsshObject) ? val.debug_name : JsshSocket.to_javascript(val)})"
961
- result=assign_expr(JsshSocket.to_javascript(val))
962
- # logger.info { "#{self.class} assigned: #{debug_name} (type #{type})" }
963
- result
964
- end
965
- # assigns the given javascript expression (string) to the reference for this object
966
- def assign_expr(val)
967
- jssh_socket.value_json("(function(val){#{ref}=val; return null;}(#{val}))")
968
- @type=nil # uncache this
969
- # don't want to use JsshSocket#assign_json because converting the result of the assignment (that is, the expression assigned) to json is error-prone and we don't really care about the result.
970
- # don't want to use JsshSocket#assign because the result can be blank and cause send_and_read to wait for data that's not coming - also
971
- # using a json function is better because it catches errors much more elegantly.
972
- # so, wrap it in a function that returns nil.
973
- self
974
- end
975
-
976
- # returns a JsshObject for the result of calling the function represented by this object, passing
977
- # the given arguments, which are converted to javascript. if this is not a function, javascript will raise an error.
978
- def pass(*args)
979
- JsshObject.new("#{ref}(#{args.map{|arg| JsshSocket.to_javascript(arg)}.join(', ')})", jssh_socket, :function_result => true, :debug_name => "#{debug_name}(#{args.map{|arg| arg.is_a?(JsshObject) ? arg.debug_name : JsshSocket.to_javascript(arg)}.join(', ')})")
980
- end
981
-
982
- # returns the value (via JsshSocket#value_json) or a JsshObject (see #val_or_object) of the return
983
- # value of this function (assumes this object is a function) passing it the given arguments (which
984
- # are converted to javascript).
985
- #
986
- # simply, it just calls self.pass(*args).val_or_object
987
- def call(*args)
988
- pass(*args).val_or_object
989
- end
990
-
991
- # assuming the javascript object represented is a constructor, this returns a new
992
- # instance passing the given arguments.
993
- #
994
- # date_class = jssh_socket.object('Date')
995
- # => #<JsshObject:0x0118eee8 type=function, debug_name=Date>
996
- # date = date_class.new
997
- # => #<JsshObject:0x01188a84 type=object, debug_name=new Date()>
998
- # date.getFullYear
999
- # => 2010
1000
- # date_class.new('october 4, 1978').getFullYear
1001
- # => 1978
1002
- def new(*args)
1003
- JsshObject.new("new #{ref}", jssh_socket, :debug_name => "new #{debug_name}").call(*args)
1004
- end
1005
-
1006
- # sets the given javascript variable to this object, and returns a JsshObject referring
1007
- # to the variable.
1008
- #
1009
- # >> foo=document.getElementById('guser').store('foo')
1010
- # => #<JsshObject:0x2dff870 @ref="foo" ...>
1011
- # >> foo.tagName
1012
- # => "DIV"
1013
- #
1014
- # the second argument is only used internally and shouldn't be used.
1015
- def store(js_variable, somewhere_meaningful=true)
1016
- stored=JsshObject.new(js_variable, jssh_socket, :function_result => false, :debug_name => somewhere_meaningful ? "(#{js_variable}=#{debug_name})" : debug_name)
1017
- stored.assign_expr(self.ref)
1018
- stored
1019
- end
1020
-
1021
- private
1022
- # takes a block which, when yielded a random key, should result in a random reference. this checks
1023
- # that the reference is not already in use and stores this object in that reference, and returns
1024
- # a JsshObject referring to the stored object.
1025
- def store_rand_named(&name_proc)
1026
- base=36
1027
- length=32
1028
- begin
1029
- name=name_proc.call(("%#{length}s"%rand(base**length).to_s(base)).tr(' ','0'))
1030
- end #while JsshObject.new(name,jssh_socket).type!='undefined'
1031
- # okay, more than one iteration is ridiculously unlikely, sure, but just to be safe.
1032
- store(name, false)
1033
- end
1034
- public
1035
-
1036
- # stores this object in a random key of the given object and returns the stored object.
1037
- def store_rand_object_key(object)
1038
- raise ArgumentError("Object is not a JsshObject: got #{object.inspect}") unless object.is_a?(JsshObject)
1039
- store_rand_named do |r|
1040
- object.sub(r).ref
1041
- end
1042
- end
1043
-
1044
- # stores this object in a random key of the designated temporary object for this socket and returns the stored object.
1045
- def store_rand_temp
1046
- store_rand_object_key(jssh_socket.temp_object)
1047
- end
1048
-
1049
- # returns a JsshObject referring to a subscript of this object, specified as a ruby object converted to
1050
- # javascript.
1051
- #
1052
- # similar to [], but [] calls #val_or_object; this always returns a JsshObject.
1053
- def sub(key)
1054
- JsshObject.new("#{ref}[#{JsshSocket.to_javascript(key)}]", jssh_socket, :debug_name => "#{debug_name}[#{key.is_a?(JsshObject) ? key.debug_name : JsshSocket.to_javascript(key)}]")
1055
- end
1056
-
1057
- # returns a JsshObject referring to a subscript of this object, or a value if it is simple (see #val_or_object)
1058
- #
1059
- # subscript is specified as ruby (converted to javascript).
1060
- def [](key)
1061
- sub(key).val_or_object(:error_on_undefined => false)
1062
- end
1063
-
1064
- # assigns the given ruby value (which is converted to javascript) to the given subscript
1065
- # (the key is also converted to javascript).
1066
- def []=(key, value)
1067
- self.sub(key).assign(value)
1068
- end
1069
-
1070
- # calls a binary operator (in javascript) with self and another operand.
1071
- #
1072
- # the operator should be string of javascript; the operand will be converted to javascript.
1073
- def binary_operator(operator, operand)
1074
- JsshObject.new("(#{ref}#{operator}#{JsshSocket.to_javascript(operand)})", jssh_socket, :debug_name => "(#{debug_name}#{operator}#{operand.is_a?(JsshObject) ? operand.debug_name : JsshSocket.to_javascript(operand)})").val_or_object
1075
- end
1076
- # addition, using the + operator in javascript
1077
- def +(operand)
1078
- binary_operator('+', operand)
1079
- end
1080
- # subtraction, using the - operator in javascript
1081
- def -(operand)
1082
- binary_operator('-', operand)
1083
- end
1084
- # division, using the / operator in javascript
1085
- def /(operand)
1086
- binary_operator('/', operand)
1087
- end
1088
- # multiplication, using the * operator in javascript
1089
- def *(operand)
1090
- binary_operator('*', operand)
1091
- end
1092
- # modulus, using the % operator in javascript
1093
- def %(operand)
1094
- binary_operator('%', operand)
1095
- end
1096
- # returns true if the javascript object represented by this is equal to the given operand.
1097
- def ==(operand)
1098
- operand.is_a?(JsshObject) && binary_operator('==', operand)
1099
- end
1100
- # javascript triple-equals (===) operator. very different from ruby's tripl-equals operator -
1101
- # in javascript this means "really really equal"; in ruby it means "sort of equal-ish"
1102
- def triple_equals(operand)
1103
- operand.is_a?(JsshObject) && binary_operator('===', operand)
1104
- end
1105
- # inequality, using the > operator in javascript
1106
- def >(operand)
1107
- binary_operator('>', operand)
1108
- end
1109
- # inequality, using the < operator in javascript
1110
- def <(operand)
1111
- binary_operator('<', operand)
1112
- end
1113
- # inequality, using the >= operator in javascript
1114
- def >=(operand)
1115
- binary_operator('>=', operand)
1116
- end
1117
- # inequality, using the <= operator in javascript
1118
- def <=(operand)
1119
- binary_operator('<=', operand)
1120
- end
1121
-
1122
- # method_missing handles unknown method calls in a way that makes it possible to write
1123
- # javascript-like syntax in ruby, to some extent.
1124
- #
1125
- # method_missing checks the attribute of the represented javascript object with with the name of the given method. if that
1126
- # attribute refers to a function, then that function is called with any given arguments
1127
- # (like #invoke does). If that attribute is undefined, an error will be raised, unless a '?'
1128
- # suffix is used (see below).
1129
- #
1130
- # method_missing will only try to deal with methods that look like /^[a-z_][a-z0-9_]*$/i - no
1131
- # special characters, only alphanumeric/underscores, starting with alpha or underscore - with
1132
- # the exception of three special behaviors:
1133
- #
1134
- # If the method ends with an equals sign (=), it does assignment - it calls #assign on the given
1135
- # attribute, with the given (single) argument, to do the assignment and returns the assigned
1136
- # value.
1137
- #
1138
- # If the method ends with a bang (!), then it will attempt to get the value of the reference,
1139
- # using JsshObject#val, which converts the javascript to json and then to ruby. For simple types
1140
- # (null, string, boolean, number), this is what gets returned anyway. With other types (usually
1141
- # the 'object' type), attempting to convert to json can raise errors or cause infinite
1142
- # recursion, so is not attempted. but if you have an object or an array that you know you can
1143
- # json-ize, you can use ! to force that.
1144
- #
1145
- # If the method ends with a question mark (?), then if the attribute is undefined, no error is
1146
- # raised (as usually happens) - instead nil is just returned.
1147
- #
1148
- # otherwise, method_missing behaves like #invoke, and returns a JsshObject, a string, a boolean,
1149
- # a number, or null.
1150
- #
1151
- # Since method_missing returns a JsshObject for javascript objects, this means that you can
1152
- # string together method_missings and the result looks rather like javascript.
1153
- #--
1154
- # $A and $H, used below, are methods of the Prototype javascript library, which add nice functional
1155
- # methods to arrays and hashes - see http://www.prototypejs.org/
1156
- # You can use these methods with method_missing just like any other:
1157
- #
1158
- # >> js_hash=jssh_socket.object('$H')
1159
- # => #<JsshObject:0x2beb598 @ref="$H" ...>
1160
- # >> js_arr=jssh_socket.object('$A')
1161
- # => #<JsshObject:0x2be40e0 @ref="$A" ...>
1162
- #
1163
- # >> js_arr.call(document.body.childNodes).pluck! :tagName
1164
- # => ["TEXTAREA", "DIV", "NOSCRIPT", "DIV", "DIV", "DIV", "BR", "TABLE", "DIV", "DIV", "DIV", "TEXTAREA", "DIV", "DIV", "SCRIPT"]
1165
- # >> js_arr.call(document.body.childNodes).pluck! :id
1166
- # => ["csi", "header", "", "ssb", "tbd", "res", "", "nav", "wml", "", "", "hcache", "xjsd", "xjsi", ""]
1167
- # >> js_hash.call(document.getElementById('tbd')).keys!
1168
- # => ["addEventListener", "appendChild", "className", "parentNode", "getElementsByTagName", "title", ...]
1169
- def method_missing(method, *args)
1170
- method=method.to_s
1171
- if method =~ /\A([a-z_][a-z0-9_]*)([=?!])?\z/i
1172
- method = $1
1173
- suffix = $2
1174
- attr(method).assign_or_call_or_val_or_object_by_suffix(suffix, *args)
1175
- else
1176
- # don't deal with any special character crap
1177
- super
1178
- end
1179
- end
1180
- # calls define_method for each key of this object as a hash. useful for tab-completing attributes
1181
- # in irb, mostly.
1182
- def define_methods! # :nodoc:
1183
- metaclass=(class << self; self; end)
1184
- keys=jssh_socket.object("function(obj) { var keys=[]; for(var key in obj) { keys.push(key); } return keys; }").pass(self).val
1185
-
1186
- keys.grep(/\A[a-z_][a-z0-9_]*\z/i).reject{|k| self.class.method_defined?(k)}.each do |key|
1187
- metaclass.send(:define_method, key) do |*args|
1188
- invoke(key, *args)
1189
- end
1190
- end
1191
- end
1192
- # returns true if this object responds to the given method (that is, it's a defined ruby method)
1193
- # or if #method_missing will handle it
1194
- def respond_to?(method, include_private = false)
1195
- super || object_respond_to?(method)
1196
- end
1197
- # returns true if the javascript object this represents responds to the given method. this does not pay attention
1198
- # to any defined ruby methods, just javascript.
1199
- def object_respond_to?(method)
1200
- method=method.to_s
1201
- if method =~ /^([a-z_][a-z0-9_]*)([=?!])?$/i
1202
- method = $1
1203
- suffix = $2
1204
- else # don't deal with any special character crap
1205
- return false
1206
- end
1207
-
1208
- if self.type=='undefined'
1209
- return false
1210
- elsif suffix=='='
1211
- if self.type=='object'
1212
- return true # yeah, you can generally assign attributes to objects
1213
- else
1214
- return false # no, you can't generally assign attributes to (boolean, number, string, null)
1215
- end
1216
- else
1217
- attr=attr(method)
1218
- return attr.type!='undefined'
1219
- end
1220
- end
1221
-
1222
- # undefine Object#id, and... anything else I think of that needs undef'ing in the future
1223
- [:id, :display].each do |method_name|
1224
- if method_defined?(method_name)
1225
- undef_method(method_name)
1226
- end
1227
- end
1228
-
1229
- # returns this object passed through the $A function of the prototype javascript library.
1230
- def to_js_array
1231
- jssh_socket.object('$A').call(self)
1232
- end
1233
- # returns this object passed through the $H function of the prototype javascript library.
1234
- def to_js_hash
1235
- jssh_socket.object('$H').call(self)
1236
- end
1237
- # returns this object passed through a javascript function which copies each key onto a blank object and rescues any errors.
1238
- def to_js_hash_safe
1239
- jssh_socket.object('$_H').call(self)
1240
- end
1241
- # returns a JsshArray representing this object
1242
- def to_array
1243
- JsshArray.new(self.ref, self.jssh_socket, :debug_name => debug_name)
1244
- end
1245
- # returns a JsshHash representing this object
1246
- def to_hash
1247
- JsshHash.new(self.ref, self.jssh_socket, :debug_name => debug_name)
1248
- end
1249
- # returns a JsshDOMNode representing this object
1250
- def to_dom
1251
- JsshDOMNode.new(self.ref, self.jssh_socket, :debug_name => debug_name)
1252
- end
1253
-
1254
- # returns a ruby Hash. each key/value pair of this object
1255
- # is represented in the returned hash.
1256
- #
1257
- # if an error is encountered trying to access the value for an attribute, then in the
1258
- # returned hash, that attribute is set to the error that was encountered rather than
1259
- # the actual value (since the value wasn't successfully retrieved).
1260
- #
1261
- # options may be specified. the only option currently supported is:
1262
- # * :recurse => a number or nil. if it's a number, then this will recurse to that
1263
- # depth. If it's nil, this won't recurse at all.
1264
- #
1265
- # below the specified recursion level, this will return this JsshObject rather than recursing
1266
- # down into it.
1267
- #
1268
- # this function isn't expected to raise any errors, since encountered errors are set as
1269
- # attribute values.
1270
- def to_ruby_hash(options={})
1271
- options={:recurse => 1}.merge(options)
1272
- return self if !options[:recurse] || options[:recurse]==0
1273
- return self if self.type!='object'
1274
- next_options=options.merge(:recurse => options[:recurse]-1)
1275
- begin
1276
- keys=self.to_hash.keys
1277
- rescue JsshError
1278
- return self
1279
- end
1280
- keys.inject({}) do |hash, key|
1281
- val=begin
1282
- self[key]
1283
- rescue JsshError
1284
- $!
1285
- end
1286
- hash[key]=if val.is_a?(JsshObject)
1287
- val.to_ruby_hash(next_options)
1288
- else
1289
- val
1290
- end
1291
- hash
1292
- end
1293
- end
1294
-
1295
- # returns an Array in which each element is the #val_or_Object of each element of this javascript array.
1296
- def to_ruby_array
1297
- self.to_array.to_a
1298
- end
1299
-
1300
- # represents this javascript object in one line, displaying the type and debug name.
1301
- def inspect
1302
- "\#<#{self.class.name}:0x#{"%.8x"%(self.hash*2)} #{[:type, :debug_name].map{|attr| attr.to_s+'='+send(attr).to_s}.join(', ')}>"
1303
- end
1304
- def pretty_print(pp) # :nodoc:
1305
- pp.object_address_group(self) do
1306
- pp.seplist([:type, :debug_name], lambda { pp.text ',' }) do |attr|
1307
- pp.breakable ' '
1308
- pp.group(0) do
1309
- pp.text attr.to_s
1310
- pp.text ': '
1311
- #pp.breakable
1312
- pp.text send(attr)
1313
- end
1314
- end
1315
- end
1316
- end
1317
- end
1318
-
1319
- # represents a node on the DOM. not substantially from JsshObject, but #inspect
1320
- # is more informative, and #dump is defined for extensive debug info.
1321
- #
1322
- # This class is mostly useful for debug, not used anywhere in production at the moment.
1323
- class JsshDOMNode < JsshObject
1324
- def inspect_stuff # :nodoc:
1325
- [:nodeName, :nodeType, :nodeValue, :tagName, :textContent, :id, :name, :value, :type, :className, :hidden].map do |attrn|
1326
- attr=attr(attrn)
1327
- if ['undefined','null'].include?(attr.type)
1328
- nil
1329
- else
1330
- [attrn, attr.val_or_object(:error_on_undefined => false)]
1331
- end
1332
- end.compact
1333
- end
1334
- # returns a string with a bunch of information about this dom node
1335
- def inspect
1336
- "\#<#{self.class.name} #{inspect_stuff.map{|(k,v)| "#{k}=#{v.inspect}"}.join(', ')}>"
1337
- end
1338
- def pretty_print(pp) # :nodoc:
1339
- pp.object_address_group(self) do
1340
- pp.seplist(inspect_stuff, lambda { pp.text ',' }) do |attr_val|
1341
- pp.breakable ' '
1342
- pp.group(0) do
1343
- pp.text attr_val.first.to_s
1344
- pp.text ': '
1345
- #pp.breakable
1346
- pp.text attr_val.last.inspect
1347
- end
1348
- end
1349
- end
1350
- end
1351
- # returns a string (most useful when written to STDOUT or to a file) consisting of this dom node
1352
- # and its child nodes, recursively. each node is one line and depth is indicated by spacing.
1353
- #
1354
- # call #dump(:recurse => n) to recurse down only n levels. default is to recurse all the way down the dom tree.
1355
- def dump(options={})
1356
- options={:recurse => nil, :level => 0}.merge(options)
1357
- next_options=options.merge(:recurse => options[:recurse] && (options[:recurse]-1), :level => options[:level]+1)
1358
- result=(" "*options[:level]*2)+self.inspect+"\n"
1359
- if options[:recurse]==0
1360
- result+=(" "*next_options[:level]*2)+"...\n"
1361
- else
1362
- self.childNodes.to_array.each do |child|
1363
- result+=child.to_dom.dump(next_options)
1364
- end
1365
- end
1366
- result
1367
- end
1368
- end
1369
-
1370
- # this class represents a javascript array - that is, a javascript object that has a 'length'
1371
- # attribute which is a non-negative integer, and returns elements at each subscript from 0
1372
- # to less than than that length.
1373
- class JsshArray < JsshObject
1374
- # yields the element at each subscript of this javascript array, from 0 to self.length.
1375
- def each
1376
- length=self.length
1377
- raise JsshError, "length #{length.inspect} is not a non-negative integer on #{self.ref}" unless length.is_a?(Integer) && length >= 0
1378
- for i in 0...length
1379
- element=self[i]
1380
- if element.is_a?(JsshObject)
1381
- # yield a more permanent reference than the array subscript
1382
- element=element.store_rand_temp
1383
- end
1384
- yield element
1385
- end
1386
- end
1387
- include Enumerable
1388
- end
1389
-
1390
- # this class represents a hash, or 'object' type in javascript.
1391
- class JsshHash < JsshObject
1392
- # returns an array of keys of this javascript object
1393
- def keys
1394
- @keys=jssh_socket.call_function(:obj => self){ "var keys=[]; for(var key in obj) { keys.push(key); } return keys;" }.val
1395
- end
1396
- # returns whether the given key is a defined key of this javascript object
1397
- def key?(key)
1398
- jssh_socket.call_function(:obj => self, :key => key){ "return key in obj;" }
1399
- end
1400
- # yields each key and value
1401
- def each(&block) # :yields: key, value
1402
- keys.each do |key|
1403
- if block.arity==1
1404
- yield [key, self[key]]
1405
- else
1406
- yield key, self[key]
1407
- end
1408
- end
1409
- end
1410
- # yields each key and value for this object
1411
- def each_pair
1412
- each do |key,value|
1413
- yield key,value
1414
- end
1415
- end
1416
-
1417
- include Enumerable
1418
- end