melomel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +0 -0
- data/LICENSE +20 -0
- data/README.md +141 -0
- data/lib/melomel/bridge/messaging.rb +112 -0
- data/lib/melomel/bridge/ui.rb +55 -0
- data/lib/melomel/bridge.rb +76 -0
- data/lib/melomel/object_proxy.rb +58 -0
- data/lib/melomel/ui.rb +44 -0
- data/lib/melomel/version.rb +3 -0
- data/lib/melomel.rb +34 -0
- data/test/helper.rb +28 -0
- data/test/test_bridge.rb +61 -0
- data/test/test_integration.rb +41 -0
- data/test/test_messaging.rb +109 -0
- data/test/test_object_proxy.rb +48 -0
- data/test/test_ui.rb +59 -0
- metadata +129 -0
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Ben Johnson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
20
|
+
|
data/README.md
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
melomel.rb -- A Ruby interface to Melomel
|
2
|
+
=========================================
|
3
|
+
|
4
|
+
## DESCRIPTION
|
5
|
+
|
6
|
+
Melomel.rb is a library that allows Ruby to communicate with an application
|
7
|
+
running within the Flash virtual machine. For more information on Melomel,
|
8
|
+
visit the Melomel repository:
|
9
|
+
|
10
|
+
http://github.com/benbjohnson/melomel
|
11
|
+
|
12
|
+
Melomel.rb follows the rules of [Semantic Versioning](http://semver.org/) and
|
13
|
+
uses [TomDoc](http://tomdoc.org/) for inline documentation.
|
14
|
+
|
15
|
+
|
16
|
+
## INSTALLATION
|
17
|
+
|
18
|
+
To install Melomel.rb simply use RubyGems:
|
19
|
+
|
20
|
+
$ [sudo] gem install melomel
|
21
|
+
|
22
|
+
Melomel needs to be embedded and running in the application you're trying to
|
23
|
+
connect to. Please see the Melomel repository for instructions on installation.
|
24
|
+
|
25
|
+
|
26
|
+
## GETTING STARTED
|
27
|
+
|
28
|
+
The first step to using Melomel is to install the Flash SWC in your application.
|
29
|
+
Once the SWC is in your application, setup in your Ruby project is simple.
|
30
|
+
|
31
|
+
In your Ruby file, simply call the `connect()` method on `Melomel`:
|
32
|
+
|
33
|
+
require 'melomel'
|
34
|
+
Melomel.connect()
|
35
|
+
|
36
|
+
The `connect()` method is a blocking method so it won't proceed until it's done.
|
37
|
+
After it's connected, there are several actions you can perform:
|
38
|
+
|
39
|
+
1. Create Object
|
40
|
+
1. Get Class
|
41
|
+
1. Get Property
|
42
|
+
1. Set Property
|
43
|
+
1. Invoke Method
|
44
|
+
|
45
|
+
## API
|
46
|
+
|
47
|
+
### Overview
|
48
|
+
|
49
|
+
Melomel communicates to the Flash virtual machine over a socket connection
|
50
|
+
using XML. The protocol is simple and is meant to proxy all data access calls
|
51
|
+
to the Flash virtual machine. This means that only primitives (strings, numbers
|
52
|
+
and booleans) are copied but all objects are accessed by reference. By proxying
|
53
|
+
objects, all data stays in the Flash virtual machine and there are no syncing
|
54
|
+
issues.
|
55
|
+
|
56
|
+
### Create Object
|
57
|
+
|
58
|
+
To create an object, use the `create_object()` method on `Melomel`:
|
59
|
+
|
60
|
+
point = Melomel.create_object('flash.geom.Point')
|
61
|
+
|
62
|
+
The object returned is a proxy object so any actions performed on it in Ruby
|
63
|
+
will be performed on the ActionScript object in the Flash virtual machine.
|
64
|
+
|
65
|
+
### Get Class
|
66
|
+
|
67
|
+
You can retrieve a class to call static methods and properties. Since classes
|
68
|
+
are objects in ActionScript, they work identically in Melomel.
|
69
|
+
|
70
|
+
app = Melomel.get_class('mx.core.FlexGlobals')
|
71
|
+
app.topLevelApplication.name = 'Melomel App!'
|
72
|
+
|
73
|
+
This Flex 4 example updates the name of the application to "Melomel App!".
|
74
|
+
|
75
|
+
### Get Property & Set Property
|
76
|
+
|
77
|
+
Getting and setting properties is handled transparently when using the object
|
78
|
+
proxies.
|
79
|
+
|
80
|
+
point = Melomel.create_object('flash.geom.Point')
|
81
|
+
point.x = 30
|
82
|
+
point.set_property('y') = 40
|
83
|
+
p "pos: #{point.x}, #{point.y}"
|
84
|
+
p "length: #{point.get_property('length')}"
|
85
|
+
|
86
|
+
Property accessors on an object proxy are automatically used to retrieve the
|
87
|
+
property value of the Flash object. You can also use the `get_property()`
|
88
|
+
method when accessing properties and the `set_property()` method when mutating
|
89
|
+
properties.
|
90
|
+
|
91
|
+
### Invoke Method
|
92
|
+
|
93
|
+
Invoking methods is also handled transparently when using object proxies.
|
94
|
+
|
95
|
+
_IMPORTANT: There a catch! Methods without any parameters must have a bang
|
96
|
+
(`!`) character appended to their method name when calling directly on an
|
97
|
+
object proxy._
|
98
|
+
|
99
|
+
clipboard = Melomel.get_class('flash.desktop.Clipboard').generalClipboard
|
100
|
+
data = clipboard.getData('air:text')
|
101
|
+
data = clipboard.invoke_method('getData', 'air:text')
|
102
|
+
clipboard.clear!()
|
103
|
+
clipboard.invoke_method('clear')
|
104
|
+
|
105
|
+
In this example, the `getData` method could be called on the object directly
|
106
|
+
because it had more than one argument. The `clear` method, however, required
|
107
|
+
that a bang character be appended to the name or that the `invoke_method` be
|
108
|
+
used.
|
109
|
+
|
110
|
+
|
111
|
+
## HACKING
|
112
|
+
|
113
|
+
To get started with working with the source, start by running Bundler to get all
|
114
|
+
your dependencies:
|
115
|
+
|
116
|
+
[sudo] bundle install
|
117
|
+
|
118
|
+
If you are adding new features, please add test coverage to all code that you
|
119
|
+
write. Run the test suite with `rake`:
|
120
|
+
|
121
|
+
rake test
|
122
|
+
|
123
|
+
|
124
|
+
## CONTRIBUTE
|
125
|
+
|
126
|
+
If you'd like to contribute to Melomel.rb, please fork the repo on GitHub:
|
127
|
+
|
128
|
+
http://github.com/benbjohnson/melomel.rb
|
129
|
+
|
130
|
+
To get all of the dependencies, install the gem first. The best way to get
|
131
|
+
your changes merged back into core is as follows:
|
132
|
+
|
133
|
+
1. Clone your fork locally
|
134
|
+
1. Create a named topic branch to contain your change
|
135
|
+
1. Code
|
136
|
+
1. All code must have RSpec test coverage. Run `rake` to ensure everything
|
137
|
+
passes.
|
138
|
+
1. If you are adding new functionality, document it in the README
|
139
|
+
1. If necessary, rebase your commits into logical chunks, without errors
|
140
|
+
1. Push the branch up to GitHub
|
141
|
+
1. Send me a pull request for your branch
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
# These add-on methods enable the message encoding and decoding.
|
4
|
+
module Melomel
|
5
|
+
class Bridge
|
6
|
+
# Creates an object in the Flash virtual machine and returns the reference
|
7
|
+
# to it.
|
8
|
+
def create_object(class_name)
|
9
|
+
send("<create class=\"#{class_name}\"/>")
|
10
|
+
parse_message_value(Nokogiri::XML(receive()).root)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Retrieves a reference to a class in the Flash virtual machine.
|
14
|
+
def get_class(class_name)
|
15
|
+
send("<get-class name=\"#{class_name}\"/>")
|
16
|
+
parse_message_value(Nokogiri::XML(receive()).root)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Retrieves a property of an object in the Flash virtual machine
|
20
|
+
def get_property(proxy_id, property)
|
21
|
+
send("<get object=\"#{proxy_id}\" property=\"#{property}\"/>")
|
22
|
+
parse_message_value(Nokogiri::XML(receive()).root)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets a property on an object in the Flash virtual machine
|
26
|
+
def set_property(proxy_id, property, value)
|
27
|
+
# Create message and format value to set
|
28
|
+
xml = Nokogiri::XML("<set object=\"#{proxy_id}\" property=\"#{property}\"><arg/></set>")
|
29
|
+
format_message_value(xml.at_xpath('/set/arg'), value)
|
30
|
+
|
31
|
+
# Send & Receive
|
32
|
+
send(xml.root.to_xml(:indent => 0))
|
33
|
+
parse_message_value(Nokogiri::XML(receive()).root)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Invokes a method on an object in the Flash virtual machine
|
37
|
+
def invoke_method(proxy_id, method_name, *args)
|
38
|
+
xml = Nokogiri::XML("<invoke object=\"#{proxy_id}\" method=\"#{method_name}\"><args/></invoke>")
|
39
|
+
|
40
|
+
# Loop over and add arguments to call
|
41
|
+
args_node = xml.at_xpath('invoke/args')
|
42
|
+
args.each do |arg|
|
43
|
+
arg_node = Nokogiri::XML::Node.new('arg', xml)
|
44
|
+
format_message_value(arg_node, arg)
|
45
|
+
args_node.add_child(arg_node)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Send and receive
|
49
|
+
send(xml.root.to_xml(:indent => 0))
|
50
|
+
parse_message_value(Nokogiri::XML(receive()).root)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creates an object proxy from a hash
|
54
|
+
def create_hash(hash)
|
55
|
+
proxy = create_object('Object')
|
56
|
+
hash.each_pair do |k,v|
|
57
|
+
v = create_hash(v) if !v.nil? && v.is_a?(Hash)
|
58
|
+
proxy.set_property(k, v)
|
59
|
+
end
|
60
|
+
return proxy
|
61
|
+
end
|
62
|
+
|
63
|
+
# Formats a Ruby value into an XML message
|
64
|
+
def format_message_value(xml, value)
|
65
|
+
# Automatically convert simple hashes to objects
|
66
|
+
if(!value.nil? && value.is_a?(Hash))
|
67
|
+
value = create_hash(value)
|
68
|
+
end
|
69
|
+
|
70
|
+
if value.nil?
|
71
|
+
xml['dataType'] = 'null'
|
72
|
+
elsif value.class == Fixnum || value.class == Bignum
|
73
|
+
xml['value'] = value.to_s
|
74
|
+
xml['dataType'] = 'int'
|
75
|
+
elsif value.class == Float
|
76
|
+
xml['value'] = value.to_s
|
77
|
+
xml['dataType'] = 'float'
|
78
|
+
elsif value == true || value == false
|
79
|
+
xml['value'] = value.to_s
|
80
|
+
xml['dataType'] = 'boolean'
|
81
|
+
elsif value.class == Melomel::ObjectProxy
|
82
|
+
xml['value'] = value.proxy_id.to_s
|
83
|
+
xml['dataType'] = 'object'
|
84
|
+
elsif value.class == String
|
85
|
+
xml['value'] = value
|
86
|
+
xml['dataType'] = 'string'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Parses a return message and converts it into an appropriate type
|
91
|
+
def parse_message_value(xml)
|
92
|
+
value = xml['value']
|
93
|
+
data_type = xml['dataType']
|
94
|
+
|
95
|
+
if data_type == 'null'
|
96
|
+
return nil
|
97
|
+
elsif data_type == 'int'
|
98
|
+
return value.to_i
|
99
|
+
elsif data_type == 'float'
|
100
|
+
return value.to_f
|
101
|
+
elsif data_type == 'boolean'
|
102
|
+
return value == 'true'
|
103
|
+
elsif data_type == 'object'
|
104
|
+
return Melomel::ObjectProxy.new(self, value.to_i)
|
105
|
+
elsif data_type == 'string' || data_type.nil?
|
106
|
+
return value
|
107
|
+
else
|
108
|
+
raise UnrecognizedTypeError, "Unknown type: #{data_type}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# These add-on methods provide ease of use utility methods.
|
2
|
+
module Melomel
|
3
|
+
class Bridge
|
4
|
+
# Finds a list of display objects matching a class and hash of properties.
|
5
|
+
def find_all(class_name, root={}, properties={})
|
6
|
+
# Merge hashes if no root is specified
|
7
|
+
if root.is_a?(Hash)
|
8
|
+
properties.merge!(root)
|
9
|
+
root = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Retrieve object
|
13
|
+
get_class('melomel.core.UI').findAll(class_name, root, properties)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Finds a display object by class and properties.
|
17
|
+
def find(class_name, root={}, properties={})
|
18
|
+
# Merge hashes if no root is specified
|
19
|
+
if root.is_a?(Hash)
|
20
|
+
properties.merge!(root)
|
21
|
+
root = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieve object
|
25
|
+
get_class('melomel.core.UI').find(class_name, root, properties)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Imitates a click on a component
|
30
|
+
def click(component, properties={})
|
31
|
+
get_class('melomel.core.UI').click(component, properties)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Imitates a double click on a component
|
35
|
+
def double_click(component, properties={})
|
36
|
+
get_class('melomel.core.UI').doubleClick(component, properties)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Imitates a key down on a component
|
41
|
+
def key_down(component, char, properties={})
|
42
|
+
get_class('melomel.core.UI').keyDown(component, char, properties)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Imitates a key up on a component
|
46
|
+
def key_up(component, char, properties={})
|
47
|
+
get_class('melomel.core.UI').keyUp(component, char, properties)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Imitates a key press on a component
|
51
|
+
def key_press(component, char, properties={})
|
52
|
+
get_class('melomel.core.UI').keyPress(component, char, properties)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'melomel/bridge/messaging'
|
3
|
+
require 'melomel/bridge/ui'
|
4
|
+
|
5
|
+
# The bridge manages the connection to the Flash virtual machine. All messages
|
6
|
+
# sent to the virtual machine are passed through this object.
|
7
|
+
module Melomel
|
8
|
+
class Bridge
|
9
|
+
attr_accessor :host, :port
|
10
|
+
|
11
|
+
def initialize(host='localhost', port=10101)
|
12
|
+
self.host = host
|
13
|
+
self.port = port
|
14
|
+
server, @socket = nil, nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# Opens a socket connection to listen for incoming bridge connections. This
|
18
|
+
# method automatically handles requests for the policy file.
|
19
|
+
def connect()
|
20
|
+
disconnect()
|
21
|
+
|
22
|
+
# Listen for connections
|
23
|
+
server = TCPServer.open(host, port)
|
24
|
+
|
25
|
+
# Retrieve socket and check for initial handshake
|
26
|
+
while(@socket.nil?) do
|
27
|
+
socket = server.accept()
|
28
|
+
data = socket.gets("\x00").chomp("\x00")
|
29
|
+
|
30
|
+
# Send policy file if requested.
|
31
|
+
if(data == '<policy-file-request/>')
|
32
|
+
send_policy_file(socket)
|
33
|
+
# Otherwise open connection and continue
|
34
|
+
elsif(data == '<connect/>')
|
35
|
+
@socket = socket
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
server.close();
|
40
|
+
end
|
41
|
+
|
42
|
+
# Closes any open connection to a Flash virtual machine.
|
43
|
+
def disconnect()
|
44
|
+
begin
|
45
|
+
@socket.close()
|
46
|
+
rescue
|
47
|
+
end
|
48
|
+
|
49
|
+
@socket = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
# Sends a message over to the Flash bridge.
|
53
|
+
def send(message)
|
54
|
+
@socket.puts("#{message}\x00")
|
55
|
+
end
|
56
|
+
|
57
|
+
# Receives a message from the Flash bridge. This is a blocking call.
|
58
|
+
def receive()
|
59
|
+
@socket.gets("\x00").chomp("\x00")
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
def send_policy_file(socket)
|
65
|
+
policy = ''
|
66
|
+
policy << '<?xml version="1.0"?>'
|
67
|
+
policy << '<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">'
|
68
|
+
policy << '<cross-domain-policy>'
|
69
|
+
policy << "<allow-access-from domain=\"#{host}\" to-ports=\"#{port}\"/>"
|
70
|
+
policy << '</cross-domain-policy>'
|
71
|
+
socket.send(policy)
|
72
|
+
socket.flush()
|
73
|
+
socket.close()
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# This class as a proxy to an object in the Flash virtual machine. Invoking
|
2
|
+
# methods, accessing properties or changing properties on this object will
|
3
|
+
# result in a command being sent to Flash to change or access the state of the
|
4
|
+
# object within the virtual machine. The Ruby object proxy holds no state.
|
5
|
+
#
|
6
|
+
# The object proxy works exactly as if the Flash object was a local Ruby object.
|
7
|
+
# To do this, the following aliases are made:
|
8
|
+
# * Method calls with method names ending in "=" are aliased to #set_property
|
9
|
+
# * Method calls without arguments are aliased to #get_property.
|
10
|
+
# * Method calls with arguments are aliased to #invoke_method
|
11
|
+
module Melomel
|
12
|
+
class ObjectProxy
|
13
|
+
attr_reader :bridge, :proxy_id
|
14
|
+
|
15
|
+
def initialize(bridge, proxy_id)
|
16
|
+
@bridge = bridge
|
17
|
+
@proxy_id = proxy_id
|
18
|
+
end
|
19
|
+
|
20
|
+
# Retrieves the value of a property for the proxied object.
|
21
|
+
def get_property(name)
|
22
|
+
@bridge.get_property(@proxy_id, name)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Sets the value of a property for the proxied object.
|
26
|
+
def set_property(name, value)
|
27
|
+
@bridge.set_property(@proxy_id, name, value)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Invokes a method on the proxied object. Arguments passed into the method
|
31
|
+
# are passed through to the invoked method
|
32
|
+
def invoke_method(method_name, *args)
|
33
|
+
@bridge.invoke_method(@proxy_id, method_name, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :invoke :invoke_method
|
37
|
+
|
38
|
+
# Proxies all methods to the appropriate Flash objects.
|
39
|
+
def method_missing(symbol, *args)
|
40
|
+
method_name = symbol.to_s
|
41
|
+
last_char = method_name.to_s[-1,1]
|
42
|
+
|
43
|
+
# Methods ending in "=" are aliased to set_property
|
44
|
+
if last_char == '='
|
45
|
+
return set_property(method_name.chop, *args)
|
46
|
+
# Methods with arguments are methods
|
47
|
+
elsif args.length > 0
|
48
|
+
return invoke_method(method_name, *args)
|
49
|
+
# Methods ending in '!' are methods
|
50
|
+
elsif last_char == '!'
|
51
|
+
return invoke_method(method_name.chop, *args)
|
52
|
+
# Methods with no arguments are aliased to get_property
|
53
|
+
else
|
54
|
+
return get_property(method_name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/melomel/ui.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# This class provides ease of use utility methods for finding display objects
|
2
|
+
# and interacting with them.
|
3
|
+
module Melomel
|
4
|
+
class UI
|
5
|
+
class << self
|
6
|
+
# Finds all display objects matching a class and hash of properties.
|
7
|
+
def find_all(class_name, root={}, properties={})
|
8
|
+
Melomel.bridge.find_all(class_name, root, properties)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Finds a display object by class and properties.
|
12
|
+
def find(class_name, root={}, properties={})
|
13
|
+
Melomel.bridge.find(class_name, root, properties)
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# Imitates a click on a component
|
18
|
+
def click(component, properties={})
|
19
|
+
Melomel.bridge.click(component, properties)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Imitates a double click on a component
|
23
|
+
def double_click(component, properties={})
|
24
|
+
Melomel.bridge.double_click(component, properties)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Imitates a key down on a component
|
29
|
+
def key_down(component, char, properties={})
|
30
|
+
Melomel.bridge.key_down(component, char, properties)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Imitates a key up on a component
|
34
|
+
def key_up(component, char, properties={})
|
35
|
+
Melomel.bridge.key_up(component, char, properties)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Imitates a key press on a component
|
39
|
+
def key_press(component, char, properties={})
|
40
|
+
Melomel.bridge.key_press(component, char, properties)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/melomel.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'melomel/bridge'
|
2
|
+
require 'melomel/object_proxy'
|
3
|
+
require 'melomel/ui'
|
4
|
+
require 'melomel/version'
|
5
|
+
|
6
|
+
# This class acts as a singleton instance of the bridge. This is typically the
|
7
|
+
# only class you'll need to use. If multiple instances of the bridge are needed
|
8
|
+
# (to connect to multiple SWF files), you will need to instantiate and manage
|
9
|
+
# the bridge objects manually.
|
10
|
+
module Melomel
|
11
|
+
class MelomelError < StandardError; end
|
12
|
+
class UnrecognizedTypeError < MelomelError; end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :bridge
|
16
|
+
|
17
|
+
# Opens a bridge connection to the SWF file. This is a blocking call and
|
18
|
+
# will wait until a SWF connects before continuing.
|
19
|
+
def connect(host='localhost', port=10101)
|
20
|
+
@bridge = Melomel::Bridge.new(host, port)
|
21
|
+
@bridge.connect();
|
22
|
+
end
|
23
|
+
|
24
|
+
# Retrieves a reference to a class
|
25
|
+
def get_class(class_name)
|
26
|
+
@bridge.get_class(class_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Creates an object in the virtual machine.
|
30
|
+
def create_object(class_name)
|
31
|
+
@bridge.create_object(class_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "bundler"
|
3
|
+
Bundler.setup
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
8
|
+
$LOAD_PATH.unshift(File.join(dir, '..', 'lib'))
|
9
|
+
$LOAD_PATH.unshift(dir)
|
10
|
+
|
11
|
+
require 'melomel'
|
12
|
+
|
13
|
+
class RunnerTestCase < MiniTest::Unit::TestCase
|
14
|
+
def start_runner
|
15
|
+
# Make sure FLEX_HOME is defined
|
16
|
+
raise 'FLEX_HOME environment variable must be set' if ENV['FLEX_HOME'].nil?
|
17
|
+
|
18
|
+
# Open up the sandbox
|
19
|
+
@pid = fork do
|
20
|
+
exec("#{ENV['FLEX_HOME']}/bin/adl target/MelomelRunner-app.xml")
|
21
|
+
end
|
22
|
+
Process.detach(@pid)
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop_runner
|
26
|
+
Process.kill('KILL', @pid)
|
27
|
+
end
|
28
|
+
end
|
data/test/test_bridge.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), *%w[helper])
|
2
|
+
|
3
|
+
class BridgeTestCase < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@bridge = Melomel::Bridge.new('localhost', 10101)
|
6
|
+
end
|
7
|
+
|
8
|
+
def connect
|
9
|
+
# Mock server
|
10
|
+
@server = mock('server')
|
11
|
+
TCPServer.stubs(:open).returns(@server)
|
12
|
+
TCPServer.any_instance.stubs(:close)
|
13
|
+
|
14
|
+
# Mock sockets
|
15
|
+
@socket = mock('socket')
|
16
|
+
@socket.expects(:gets).returns("<connect/>\x00")
|
17
|
+
@server.expects(:accept).returns(@socket)
|
18
|
+
@server.expects(:close)
|
19
|
+
|
20
|
+
# Attempt connection
|
21
|
+
@bridge.connect()
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_should_send_messages_over_socket_connection
|
25
|
+
connect()
|
26
|
+
@socket.expects(:puts).with("<message/>\x00")
|
27
|
+
@bridge.send('<message/>')
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_receive_messages_from_socket_connection
|
31
|
+
connect()
|
32
|
+
@socket.expects(:gets).returns("<message/>\x00")
|
33
|
+
assert_equal '<message/>', @bridge.receive()
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_should_send_policy_file_and_connect
|
37
|
+
policy = '<?xml version="1.0"?><!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd"><cross-domain-policy><allow-access-from domain="localhost" to-ports="10101"/></cross-domain-policy>';
|
38
|
+
|
39
|
+
# Mock server
|
40
|
+
server = mock('server')
|
41
|
+
server.expects(:close)
|
42
|
+
TCPServer.expects(:open).returns(server)
|
43
|
+
|
44
|
+
# Mock policy file
|
45
|
+
policy_socket = mock('policy_socket')
|
46
|
+
policy_socket.expects(:gets).returns("<policy-file-request/>\x00")
|
47
|
+
policy_socket.expects(:send).with(policy)
|
48
|
+
policy_socket.expects(:flush)
|
49
|
+
policy_socket.expects(:close)
|
50
|
+
|
51
|
+
# Mock regular socket
|
52
|
+
socket = mock('socket')
|
53
|
+
socket.expects(:gets).returns("<connect/>\x00")
|
54
|
+
|
55
|
+
# Server should return policy socket first and then regular socket
|
56
|
+
server.stubs(:accept).returns(policy_socket, socket, nil)
|
57
|
+
|
58
|
+
# Attempt connection
|
59
|
+
@bridge.connect()
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), *%w[helper])
|
2
|
+
|
3
|
+
class IntegrationTestCase < RunnerTestCase
|
4
|
+
def setup
|
5
|
+
start_runner
|
6
|
+
Melomel.connect()
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
stop_runner
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_get_application_name
|
14
|
+
app = Melomel.get_class('mx.core.FlexGlobals')
|
15
|
+
assert_equal 'Melomel Runner', app.topLevelApplication.name
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_should_get_property
|
19
|
+
runner = Melomel.get_class('MelomelRunner')
|
20
|
+
assert_equal 'bar', runner.foo
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_should_set_property
|
24
|
+
runner = Melomel.get_class('MelomelRunner')
|
25
|
+
runner.name = 'Susy'
|
26
|
+
assert_equal 'Susy', runner.name
|
27
|
+
runner.name = 'John' # TODO: Do not make other tests dependent on this
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_invoke_method
|
31
|
+
runner = Melomel.get_class('MelomelRunner')
|
32
|
+
assert_equal 'Hello, John', runner.hello('John')
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_should_create_object
|
36
|
+
point = Melomel.create_object('flash.geom.Point')
|
37
|
+
point.x = 30
|
38
|
+
point.y = 40
|
39
|
+
assert_equal 50, point.length
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), *%w[helper])
|
2
|
+
|
3
|
+
class MessagingTestCase < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@bridge = Melomel::Bridge.new('localhost', 10101)
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
#############################################################################
|
10
|
+
#
|
11
|
+
# Message Parsing
|
12
|
+
#
|
13
|
+
#############################################################################
|
14
|
+
|
15
|
+
def test_should_parse_null
|
16
|
+
message = '<return dataType="null"/>'
|
17
|
+
assert_nil @bridge.parse_message_value(Nokogiri::XML(message).root)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_should_parse_integer
|
21
|
+
message = '<return value="12" dataType="int"/>'
|
22
|
+
assert_equal 12, @bridge.parse_message_value(Nokogiri::XML(message).root)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_should_parse_float
|
26
|
+
message = '<return value="100.12" dataType="float"/>'
|
27
|
+
assert_equal 100.12, @bridge.parse_message_value(Nokogiri::XML(message).root)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_should_parse_boolean_true
|
31
|
+
message = '<return value="true" dataType="boolean"/>'
|
32
|
+
assert_equal true, @bridge.parse_message_value(Nokogiri::XML(message).root)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_should_parse_boolean_false
|
36
|
+
message = '<return value="false" dataType="boolean"/>'
|
37
|
+
assert_equal false, @bridge.parse_message_value(Nokogiri::XML(message).root)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_should_parse_string
|
41
|
+
message = '<return value="foo" dataType="string"/>'
|
42
|
+
assert_equal 'foo', @bridge.parse_message_value(Nokogiri::XML(message).root)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_should_parse_object_proxy
|
46
|
+
message = '<return value="123" dataType="object"/>'
|
47
|
+
proxy = @bridge.parse_message_value(Nokogiri::XML(message).root)
|
48
|
+
assert_instance_of Melomel::ObjectProxy, proxy
|
49
|
+
assert_equal 123, proxy.proxy_id
|
50
|
+
assert_equal @bridge, proxy.bridge
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_should_throw_error_parsing_unknown_type
|
54
|
+
message = '<return value="foo" dataType="unknown_type"/>'
|
55
|
+
assert_raises Melomel::UnrecognizedTypeError do
|
56
|
+
@bridge.parse_message_value(Nokogiri::XML(message).root)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
#############################################################################
|
62
|
+
#
|
63
|
+
# Message Formatting
|
64
|
+
#
|
65
|
+
#############################################################################
|
66
|
+
|
67
|
+
def test_should_format_nil
|
68
|
+
xml = Nokogiri::XML('<root/>').root
|
69
|
+
@bridge.format_message_value(xml, nil)
|
70
|
+
assert_equal '<root dataType="null"/>', xml.to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_should_format_integer
|
74
|
+
xml = Nokogiri::XML('<root/>').root
|
75
|
+
@bridge.format_message_value(xml, 12)
|
76
|
+
assert_equal '<root value="12" dataType="int"/>', xml.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def should_format_float
|
80
|
+
xml = Nokogiri::XML('<root/>').root
|
81
|
+
@bridge.format_message_value(xml, 100.12)
|
82
|
+
assert_equal '<root value="100.12" dataType="float"/>', xml.to_s
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_should_format_boolean_true
|
86
|
+
xml = Nokogiri::XML('<root/>').root
|
87
|
+
@bridge.format_message_value(xml, true)
|
88
|
+
assert_equal '<root value="true" dataType="boolean"/>', xml.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_should_format_boolean_false
|
92
|
+
xml = Nokogiri::XML('<root/>').root
|
93
|
+
@bridge.format_message_value(xml, false)
|
94
|
+
assert_equal '<root value="false" dataType="boolean"/>', xml.to_s
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_should_format_string
|
98
|
+
xml = Nokogiri::XML('<root/>').root
|
99
|
+
@bridge.format_message_value(xml, 'foo')
|
100
|
+
assert_equal '<root value="foo" dataType="string"/>', xml.to_s
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_should_format_object
|
104
|
+
proxy = Melomel::ObjectProxy.new(@bridge, 123)
|
105
|
+
xml = Nokogiri::XML('<root/>').root
|
106
|
+
@bridge.format_message_value(xml, proxy)
|
107
|
+
assert_equal '<root value="123" dataType="object"/>', xml.to_s
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), *%w[helper])
|
2
|
+
|
3
|
+
class ObjectProxyTestCase < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@bridge = mock()
|
6
|
+
@proxy = Melomel::ObjectProxy.new(@bridge, 123)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_should_delegate_to_bridge_to_retrieve_property
|
10
|
+
@bridge.expects(:get_property).with(123, 'foo')
|
11
|
+
@proxy.get_property('foo')
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_should_delegate_to_bridge_to_set_property
|
15
|
+
@bridge.expects(:set_property).with(123, 'foo', 'bar')
|
16
|
+
@proxy.set_property('foo', 'bar')
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_should_delegate_to_bridge_to_invoke_method
|
20
|
+
@bridge.expects(:invoke_method).with(123, 'foo', 'bar', 'baz')
|
21
|
+
@proxy.invoke_method('foo', 'bar', 'baz')
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_should_alias_invoke_to_invoke_method
|
25
|
+
@bridge.expects(:invoke_method).with(123, 'foo')
|
26
|
+
@proxy.invoke('foo')
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_should_alias_accessors_to_get_property_method
|
30
|
+
@proxy.expects(:get_property).with('foo')
|
31
|
+
@proxy.foo
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_should_alias_mutators_to_set_property_method
|
35
|
+
@proxy.expects(:set_property).with('foo', 'bar')
|
36
|
+
@proxy.foo = 'bar'
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_should_alias_methods_with_arguments_to_invoke_method
|
40
|
+
@proxy.expects(:invoke_method).with('foo', 'bar', 'baz')
|
41
|
+
@proxy.foo('bar', 'baz')
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_should_alias_methods_ending_in_bang_to_invoke_method
|
45
|
+
@proxy.expects(:invoke_method).with('foo')
|
46
|
+
@proxy.foo!()
|
47
|
+
end
|
48
|
+
end
|
data/test/test_ui.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), *%w[helper])
|
2
|
+
|
3
|
+
class IntegrationTestCase < RunnerTestCase
|
4
|
+
def setup
|
5
|
+
start_runner
|
6
|
+
Melomel.connect()
|
7
|
+
end
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
stop_runner
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_should_find_list_of_labels_named_foo
|
14
|
+
labels = Melomel::UI.find_all('spark.components.Label', :name => 'foo')
|
15
|
+
assert_equal 3, labels.length
|
16
|
+
end
|
17
|
+
|
18
|
+
def should_find_text_input
|
19
|
+
text_input = Melomel::UI.find('spark.components.TextInput', :id => 'nameTextInput')
|
20
|
+
assert_equal 'nameTextField', text_input.name
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def test_should_click_button
|
25
|
+
button = Melomel::UI.find('spark.components.Button', :id => 'clickButton')
|
26
|
+
label = Melomel::UI.find('spark.components.Label', :id => 'clickLabel')
|
27
|
+
Melomel::UI.click(button)
|
28
|
+
assert_equal 'Hello!', label.text
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_should_double_click_the_button
|
32
|
+
button = Melomel::UI.find('spark.components.Button', :id => 'doubleClickButton')
|
33
|
+
label = Melomel::UI.find('spark.components.Label', :id => 'doubleClickLabel')
|
34
|
+
Melomel::UI.double_click(button)
|
35
|
+
assert_equal 'Hello Hello!', label.text
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def test_should_press_key_down
|
40
|
+
text_input = Melomel::UI.find('spark.components.TextInput', :id => 'keyDownTextInput')
|
41
|
+
label = Melomel::UI.find('spark.components.Label', :id => 'keyDownLabel')
|
42
|
+
Melomel::UI.key_down(text_input, 'a')
|
43
|
+
assert_equal 'a', label.text
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_should_release_key_up
|
47
|
+
text_input = Melomel::UI.find('spark.components.TextInput', :id => 'keyUpTextInput')
|
48
|
+
label = Melomel::UI.find('spark.components.Label', :id => 'keyUpLabel')
|
49
|
+
Melomel::UI.key_up(text_input, 'b')
|
50
|
+
assert_equal 'b', label.text
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_should_press_key
|
54
|
+
text_input = Melomel::UI.find('spark.components.TextInput', :id => 'keyPressTextInput')
|
55
|
+
label = Melomel::UI.find('spark.components.Label', :id => 'keyPressLabel')
|
56
|
+
Melomel::UI.key_press(text_input, 'a')
|
57
|
+
assert_equal 'du', label.text
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: melomel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ben Johnson
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-08-26 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: nokogiri
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 4
|
30
|
+
- 3
|
31
|
+
version: 1.4.3
|
32
|
+
type: :runtime
|
33
|
+
prerelease: false
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: minitest
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 1
|
44
|
+
- 7
|
45
|
+
- 0
|
46
|
+
version: 1.7.0
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: mocha
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
- 9
|
60
|
+
- 8
|
61
|
+
version: 0.9.8
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: *id003
|
65
|
+
description: Melomel allows Rubyist to communicate with Flash and Flex applications easily
|
66
|
+
email:
|
67
|
+
- benbjohnson@yahoo.com
|
68
|
+
executables: []
|
69
|
+
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files: []
|
73
|
+
|
74
|
+
files:
|
75
|
+
- lib/melomel/bridge/messaging.rb
|
76
|
+
- lib/melomel/bridge/ui.rb
|
77
|
+
- lib/melomel/bridge.rb
|
78
|
+
- lib/melomel/object_proxy.rb
|
79
|
+
- lib/melomel/ui.rb
|
80
|
+
- lib/melomel/version.rb
|
81
|
+
- lib/melomel.rb
|
82
|
+
- LICENSE
|
83
|
+
- README.md
|
84
|
+
- CHANGELOG.md
|
85
|
+
- test/helper.rb
|
86
|
+
- test/test_bridge.rb
|
87
|
+
- test/test_integration.rb
|
88
|
+
- test/test_messaging.rb
|
89
|
+
- test/test_object_proxy.rb
|
90
|
+
- test/test_ui.rb
|
91
|
+
has_rdoc: true
|
92
|
+
homepage: http://github.com/benbjohnson/melomel.rb
|
93
|
+
licenses: []
|
94
|
+
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
version: "0"
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
requirements: []
|
117
|
+
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.3.7
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: A Ruby interface to Melomel
|
123
|
+
test_files:
|
124
|
+
- test/helper.rb
|
125
|
+
- test/test_bridge.rb
|
126
|
+
- test/test_integration.rb
|
127
|
+
- test/test_messaging.rb
|
128
|
+
- test/test_object_proxy.rb
|
129
|
+
- test/test_ui.rb
|