melomel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|