graffle 0.1.7 → 0.1.8
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/History.txt +5 -4
- data/Manifest.txt +13 -10
- data/Rakefile.hoe +1 -1
- data/examples/rails-workflow-test.expected +10 -0
- data/examples/rails-workflow-test.graffle/data.plist +1033 -0
- data/examples/rails-workflow-test.graffle/image1.png +0 -0
- data/examples/rails-workflow-test.graffle/image2.png +0 -0
- data/examples/rails-workflow-test.graffle/image3.png +0 -0
- data/examples/rails-workflow-test.rb +40 -0
- data/graffle.tmproj +431 -2
- data/lib/graffle/stereotypes.rb +15 -4
- data/lib/graffle/styled-text-reader.rb +3 -0
- data/lib/graffle/version.rb +1 -1
- data/lib/graphical_tests_for_rails.rb +25 -0
- data/lib/graphical_tests_for_rails/interpreters.rb +147 -0
- data/lib/graphical_tests_for_rails/orderings.rb +101 -0
- data/test/graffle-file-types/opening-tests.rb +0 -1
- data/test/graphical_tests_for_rails/graphic-interpreter-tests.rb +131 -0
- data/test/graphical_tests_for_rails/in-workflow-order-tests.rb +165 -0
- data/test/{grails-tests/commands-from-quoted-args-tests.rb → graphical_tests_for_rails/text-interpreter-tests.rb} +38 -27
- data/test/line-graphic-tests.rb +8 -0
- data/test/sheet-tests.rb +18 -0
- data/test/tests-of-examples/workflow-slowtests.rb +19 -0
- metadata +20 -18
- data/bin/bin-skeleton +0 -13
- data/lib/graffle/lib-skeleton +0 -3
- data/lib/grail_test.rb +0 -16
- data/lib/grail_test/command-interpreters.rb +0 -78
- data/test/grails-tests/destinations-tests.rb +0 -19
- data/test/grails-tests/do-nothing-commands-tests.rb +0 -18
- data/test/grails-tests/graphic-interpreter-tests.rb +0 -70
- data/test/grails-tests/translation-testcase.rb +0 -25
- data/test/test-skeleton +0 -19
data/lib/graffle/stereotypes.rb
CHANGED
@@ -97,9 +97,14 @@ module Graffle
|
|
97
97
|
self.origin < other.origin
|
98
98
|
end
|
99
99
|
|
100
|
+
# Is this object the label for some LineGraphic?
|
101
|
+
def is_line_label?; self.has_key?('Line'); end
|
102
|
+
|
100
103
|
# A Point. Must be defined in the whatever includes this module.
|
101
104
|
def origin; includer_responsibility; end
|
102
105
|
|
106
|
+
|
107
|
+
|
103
108
|
end
|
104
109
|
|
105
110
|
|
@@ -172,9 +177,6 @@ module Graffle
|
|
172
177
|
# double-click on the object.)
|
173
178
|
def has_content?; self.has_key?('Text'); end
|
174
179
|
|
175
|
-
# Is this object the label for some LineGraphic?
|
176
|
-
def is_line_label?; self.has_key?('Line'); end
|
177
|
-
|
178
180
|
def act_as_line_label # :nodoc:
|
179
181
|
|
180
182
|
def self.for_line(graffle_id) # :nodoc:
|
@@ -316,7 +318,9 @@ module Graffle
|
|
316
318
|
def go_to(thing); _link__bemhack('Head', thing); end
|
317
319
|
|
318
320
|
def _follow__bemhack(which)
|
319
|
-
|
321
|
+
endpoint = self[which]
|
322
|
+
return nil unless endpoint
|
323
|
+
container.find_by_id(endpoint['ID'])
|
320
324
|
end
|
321
325
|
|
322
326
|
# TODO: note this is no good for updating, since fields other than
|
@@ -385,6 +389,13 @@ module Graffle
|
|
385
389
|
self['GraphicsList']
|
386
390
|
end
|
387
391
|
|
392
|
+
# Labels for lines are independent ShapedGraphic objects that
|
393
|
+
# point to LineGraphic objects, so graphics will return them.
|
394
|
+
# This method omits them.
|
395
|
+
def graphics_without_labels
|
396
|
+
graphics.reject { |g| g.is_line_label? }
|
397
|
+
end
|
398
|
+
|
388
399
|
# Only the LineGraphic objects within the Sheet.
|
389
400
|
def all_lines
|
390
401
|
graphics.find_all do | g |
|
@@ -3,6 +3,9 @@
|
|
3
3
|
# Created by Brian Marick on 2007-07-06.
|
4
4
|
# Copyright (c) 2007. All rights reserved.
|
5
5
|
|
6
|
+
# ActiveSupport also defines starts_with? but for some reason the tests
|
7
|
+
# hang if I try to require it instead.
|
8
|
+
require 'extensions/string' unless String.method_defined?('starts_with?')
|
6
9
|
|
7
10
|
module Graffle
|
8
11
|
|
data/lib/graffle/version.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-10.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
require 'graffle'
|
9
|
+
require 'graphical_tests_for_rails/interpreters'
|
10
|
+
require 'graphical_tests_for_rails/orderings'
|
11
|
+
|
12
|
+
# Classes within this module help you build up Rails integration tests
|
13
|
+
# from OmniGraffle files. The important classes are these:
|
14
|
+
#
|
15
|
+
# * GraphicInterpreter: These objects are 'programmed' with
|
16
|
+
# AbstractMessageMakers who know how to convert lines of text
|
17
|
+
# into messages and arguments to send to a _target_.
|
18
|
+
# * GraphicOrderer: These objects take a list of graphics in no
|
19
|
+
# particular order and put it in the right order for a particular
|
20
|
+
# style of test. For example, a WorkflowOrder represents an
|
21
|
+
# order created by tracing through shapes connected by lines.
|
22
|
+
|
23
|
+
module GraphicalTestsForRails
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
require 'test/unit/assertionfailederror'
|
3
|
+
|
4
|
+
module GraphicalTestsForRails
|
5
|
+
|
6
|
+
# A GraphicInterpreter takes a list of _graphics_, some
|
7
|
+
# instructions about how to interpret them, and a _target_.
|
8
|
+
# It interprets the instructions to create messages that it then
|
9
|
+
# sends to the target.
|
10
|
+
#
|
11
|
+
class GraphicInterpreter
|
12
|
+
|
13
|
+
# The _graphics_ are an array of OmniGraffle AbstractGraphic objects. The
|
14
|
+
# _instructions_ map a string to an object that must respond to
|
15
|
+
# produce_method_call. (See AbstractMessageMaker#produce_method_call
|
16
|
+
# for more.)
|
17
|
+
#
|
18
|
+
# Here are the available instructions (keys in _instructions_):
|
19
|
+
#
|
20
|
+
# * ['line labels' => ...]: each line within the (single)
|
21
|
+
# label on the line is to be interpreted as a message-send by an
|
22
|
+
# instance of the given class.
|
23
|
+
# * ['shaped graphic text' => ...]: each line of content
|
24
|
+
# within the graphic is to be interpreted by the given class. The
|
25
|
+
# content is the text you type in an object after double-clicking
|
26
|
+
# on it.
|
27
|
+
# * ['text...' => ...]: a line ending in ellipses is treated this way,
|
28
|
+
# no matter what other instructions say.
|
29
|
+
#
|
30
|
+
def initialize(graphics, instructions = {})
|
31
|
+
@graphics = graphics
|
32
|
+
@line_label_strategy =
|
33
|
+
instructions['line labels']
|
34
|
+
@shaped_graphic_text_strategy =
|
35
|
+
instructions['shaped graphic text']
|
36
|
+
|
37
|
+
@ellipses_override = instructions['text...']
|
38
|
+
end
|
39
|
+
|
40
|
+
# Apply the _graphics_, according to the _instructions_, against
|
41
|
+
# the _target_.
|
42
|
+
def run_against(target)
|
43
|
+
@target = target
|
44
|
+
@log = []
|
45
|
+
@graphics.each do |g|
|
46
|
+
case g.stereotype.basename
|
47
|
+
when "ShapedGraphic"
|
48
|
+
process_commands(@shaped_graphic_text_strategy,
|
49
|
+
g.content.as_lines)
|
50
|
+
when "LineGraphic"
|
51
|
+
process_commands(@line_label_strategy,
|
52
|
+
g.label_lines)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def process_commands(command_maker, command_lines)
|
60
|
+
return if command_maker.nil?
|
61
|
+
command_lines.each do |line|
|
62
|
+
info = if /\.\.\.\s*$/ =~ line && @ellipses_override
|
63
|
+
@ellipses_override.produce_method_call(line)
|
64
|
+
else
|
65
|
+
command_maker.produce_method_call(line)
|
66
|
+
end
|
67
|
+
begin
|
68
|
+
@log << readable(*info)
|
69
|
+
@target.send(*info)
|
70
|
+
rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
|
71
|
+
raise e.class.new(%Q{#{e.message} after\n#{@log.join("\n")}})
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def readable(message, *args)
|
77
|
+
inspected = args.collect { |a| a.inspect }
|
78
|
+
"+ #{message}(#{inspected.join(', ')})"
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
class AbstractMessageMaker
|
84
|
+
# produce_method_call is given a string that is derived from some
|
85
|
+
# text somewhere associated with a graphic (label, contents of text
|
86
|
+
# box, annotation). It must answer an array whose first element is a
|
87
|
+
# message name. That message, along with any remaining array elements,
|
88
|
+
# is sent to the target.
|
89
|
+
#
|
90
|
+
# This class only exists so there's a place to hang this comment.
|
91
|
+
def produce_method_call; subclass_responsibility; end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
class ArgsFromQuotedText < AbstractMessageMaker
|
96
|
+
|
97
|
+
# The arguments are quoted within the _string_.
|
98
|
+
# The message is the text before the first arg, snake-cased.
|
99
|
+
def produce_method_call(string)
|
100
|
+
quote_marker = '%<<<==-'
|
101
|
+
|
102
|
+
string = string.gsub(/(\w)'(\w)/, "#{quote_marker}\\1\\2#{quote_marker}")
|
103
|
+
tokens = string.split(/['"],?/, allow_trailing_null_fields = -1)
|
104
|
+
message = tokens[0].strip.downcase
|
105
|
+
args = tokens[1..-1]
|
106
|
+
|
107
|
+
|
108
|
+
message.gsub!(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1\\2")
|
109
|
+
message.gsub!(/\s+/,'_')
|
110
|
+
message.gsub!(/-/, '_')
|
111
|
+
message.gsub!(/\W/, '')
|
112
|
+
|
113
|
+
|
114
|
+
args = args.enum_slice(2).collect { |e| e[0] }
|
115
|
+
args = args.collect do | arg |
|
116
|
+
arg.gsub(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1'\\2")
|
117
|
+
end
|
118
|
+
|
119
|
+
[message] + args
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
class TextIsNameOfPage
|
125
|
+
|
126
|
+
# When producing a method call, objects of this class will
|
127
|
+
# prepend the _message_ to
|
128
|
+
# a single argument it constructs.
|
129
|
+
def initialize(message)
|
130
|
+
@message = message
|
131
|
+
end
|
132
|
+
|
133
|
+
# All of the text in the _string_ is expected to be the name
|
134
|
+
# of a page. It is downcased and treated as the single argument to
|
135
|
+
# the message passed to +new+.
|
136
|
+
def produce_method_call(string)
|
137
|
+
[@message, string.downcase.strip]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Ineffectual # :nodoc:
|
142
|
+
def produce_method_call(string)
|
143
|
+
[:object_id] # no_op
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-11.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'graffle'
|
7
|
+
|
8
|
+
module GraphicalTestsForRails
|
9
|
+
|
10
|
+
# A graphic order takes a Sheet in its constructor. The graphics
|
11
|
+
# method returns an array of AbstractGraphic objects that represents
|
12
|
+
# an ordered sequence of parts of a test.
|
13
|
+
#
|
14
|
+
# This abstract class only exists so there's a place to hang this comment.
|
15
|
+
class GraphicOrderer
|
16
|
+
def graphics; subclass_responsibility; end
|
17
|
+
end
|
18
|
+
|
19
|
+
# (The following is much more obvious if you look at a sample document.)
|
20
|
+
# TODO: make an examples directory.
|
21
|
+
#
|
22
|
+
# The sheet contains one or more _rivulets_ of ShapedGraphic objects connected
|
23
|
+
# with LineGraphic objects. All together, they're supposed to represent
|
24
|
+
# a user workflow. (The gap between one rivulet and the next typically
|
25
|
+
# means that the user has gone away from the app, then returned.)
|
26
|
+
#
|
27
|
+
# graphics produces a concatenated array of the
|
28
|
+
# rivulets (both the ShapedGraphic and LineGraphic objects).
|
29
|
+
#
|
30
|
+
# Suppose there's more than one rivulet on the Sheet. graphics produces all
|
31
|
+
# the elements of the first, followed by all the elements of the second,
|
32
|
+
# etc. The first rivulet is the one _headed_ by the highest object on the
|
33
|
+
# page. After all of its elements have been placed in the graphics array,
|
34
|
+
# the next rivulet is headed by the highest remaining element on the Sheet.
|
35
|
+
#
|
36
|
+
# Within a single rivulet, location is irrelevant. All that matters is
|
37
|
+
# where lines start and end.
|
38
|
+
#
|
39
|
+
# Notice that the head of the second rivulet may be higher than some
|
40
|
+
# element of the first.
|
41
|
+
#
|
42
|
+
# A rivulet may have a single element.
|
43
|
+
|
44
|
+
class InWorkflowOrder < GraphicOrderer
|
45
|
+
include Graffle
|
46
|
+
|
47
|
+
def initialize(sheet)
|
48
|
+
@sheet = sheet
|
49
|
+
end
|
50
|
+
|
51
|
+
def graphics()
|
52
|
+
@retval = []
|
53
|
+
@source = @sheet.graphics_without_labels.sort_by {|g| g.origin}
|
54
|
+
def @source.delete_this_id(graffle_id)
|
55
|
+
delete_if { |g| g.graffle_id == graffle_id }
|
56
|
+
end
|
57
|
+
|
58
|
+
while (not @source.empty?)
|
59
|
+
something = @source.shift
|
60
|
+
@retval << something
|
61
|
+
trace_from_something(something)
|
62
|
+
end
|
63
|
+
@retval
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def trace_from_something(something)
|
69
|
+
if something.behaves_like?(LineGraphic)
|
70
|
+
trace_from_line(something)
|
71
|
+
elsif something.behaves_like?(ShapedGraphic)
|
72
|
+
trace_from_shaped(something)
|
73
|
+
else
|
74
|
+
raise "Not supposed to do this with groups yet"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def trace_from_line(line)
|
79
|
+
# Strictly, a line could connect to another line. How is that
|
80
|
+
# to be interpreted as a workflow, though?
|
81
|
+
shaped = line.to
|
82
|
+
if shaped
|
83
|
+
@retval << shaped
|
84
|
+
@source.delete_this_id(shaped.graffle_id)
|
85
|
+
trace_from_shaped(shaped)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def trace_from_shaped(shaped)
|
90
|
+
line = @source.find do |g|
|
91
|
+
g.behaves_like?(LineGraphic) && g.from == shaped
|
92
|
+
end
|
93
|
+
if line
|
94
|
+
@retval << line
|
95
|
+
@source.delete_this_id(line.graffle_id)
|
96
|
+
trace_from_line(line)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -17,7 +17,6 @@ class TestOpeningOfDocuments < Test::Unit::TestCase
|
|
17
17
|
def test_reading_of_plain_file
|
18
18
|
document = Graffle.parse(relative("multiple-canvases.graffle"))
|
19
19
|
assert_equal(3, document.sheets.length)
|
20
|
-
#Å pp document.sheets[0].graphics[0]
|
21
20
|
end
|
22
21
|
|
23
22
|
def test_reading_of_package
|
@@ -0,0 +1,131 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-10.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
require 's4t-utils'
|
8
|
+
include S4tUtils
|
9
|
+
|
10
|
+
require "../set-standalone-test-paths.rb" unless $started_from_rakefile
|
11
|
+
|
12
|
+
require 'extensions/string'
|
13
|
+
require 'graphical_tests_for_rails'
|
14
|
+
|
15
|
+
|
16
|
+
class GraphicInterpreterTest < Test::Unit::TestCase
|
17
|
+
include Graffle::Builders
|
18
|
+
include GraphicalTestsForRails
|
19
|
+
|
20
|
+
class CommandRecorder
|
21
|
+
attr_reader :record
|
22
|
+
def initialize
|
23
|
+
@record = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def method_missing(message, *args)
|
27
|
+
@record << "#{message}(" + args.collect { | a | a.inspect }.join(", ") + ')'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def two_graphics
|
32
|
+
s = sheet {
|
33
|
+
with line_label {
|
34
|
+
for_line 1
|
35
|
+
with_text 'produce "argument"'
|
36
|
+
}
|
37
|
+
with line_graphic {
|
38
|
+
graffle_id_is 1
|
39
|
+
}
|
40
|
+
with shaped_graphic {
|
41
|
+
graffle_id_is 2
|
42
|
+
with_text "SIGNUP"
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
[s.find_by_id(1), s.find_by_id(2)]
|
47
|
+
end
|
48
|
+
|
49
|
+
def assert_commands_produced(expected, interpreter)
|
50
|
+
recorder = CommandRecorder.new
|
51
|
+
interpreter.run_against(recorder)
|
52
|
+
assert_equal(expected, recorder.record)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_interpreter_applies_strategies
|
56
|
+
interpreter = GraphicInterpreter.new(two_graphics,
|
57
|
+
'line labels' => ArgsFromQuotedText.new,
|
58
|
+
'shaped graphic text' => TextIsNameOfPage.new('assert_on_page'))
|
59
|
+
assert_commands_produced(['produce("argument")', 'assert_on_page("signup")'],
|
60
|
+
interpreter)
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def test_interpreter_can_omit_strategies
|
65
|
+
interpreter = GraphicInterpreter.new(two_graphics)
|
66
|
+
assert_commands_produced([], interpreter)
|
67
|
+
end
|
68
|
+
|
69
|
+
def assert_that_fails(page_name)
|
70
|
+
assert_equal('something that does not match', page_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_interpreter_annotates_test_failures_with_log
|
74
|
+
interpreter = GraphicInterpreter.new(two_graphics,
|
75
|
+
'shaped graphic text' => TextIsNameOfPage.new('assert_that_fails'))
|
76
|
+
interpreter.run_against(self)
|
77
|
+
flunk("Should be unreached.")
|
78
|
+
rescue Test::Unit::AssertionFailedError => e
|
79
|
+
assert_match(/\+ assert_that_fails\("signup"\)/, e.message)
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_interpreter_annotates_test_errors_with_log
|
83
|
+
interpreter = GraphicInterpreter.new(two_graphics,
|
84
|
+
'line labels' => ArgsFromQuotedText.new) # will generate nonexistent method
|
85
|
+
interpreter.run_against(self)
|
86
|
+
flunk("Should be unreached.")
|
87
|
+
rescue Exception => e
|
88
|
+
assert_match(/\+ produce\("argument"\)/, e.message)
|
89
|
+
end
|
90
|
+
|
91
|
+
def four_graphics_including_ellipses
|
92
|
+
s = sheet {
|
93
|
+
with line_label {
|
94
|
+
for_line 1
|
95
|
+
with_text 'label'
|
96
|
+
}
|
97
|
+
with line_graphic {
|
98
|
+
graffle_id_is 1
|
99
|
+
}
|
100
|
+
with shaped_graphic {
|
101
|
+
graffle_id_is 2
|
102
|
+
with_text "signup "
|
103
|
+
}
|
104
|
+
with line_label {
|
105
|
+
for_line 11
|
106
|
+
with_text 'waiting on a "friend"... '
|
107
|
+
}
|
108
|
+
with line_graphic {
|
109
|
+
graffle_id_is 11
|
110
|
+
}
|
111
|
+
with shaped_graphic {
|
112
|
+
graffle_id_is 22
|
113
|
+
with_text "time passes..."
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
[1, 2, 11, 22].collect { |id| s.find_by_id(id) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_ellipses_strategy_overrides_others
|
121
|
+
interpreter = GraphicInterpreter.new(four_graphics_including_ellipses,
|
122
|
+
'line labels' => TextIsNameOfPage.new("line_here"),
|
123
|
+
'shaped graphic text' => TextIsNameOfPage.new('shape_here'),
|
124
|
+
'text...' => ArgsFromQuotedText.new)
|
125
|
+
assert_commands_produced(['line_here("label")', 'shape_here("signup")',
|
126
|
+
'waiting_on_a("friend")', 'time_passes()'],
|
127
|
+
interpreter)
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
end
|