vapir-firefox 1.8.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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