graffle 0.1.8 → 0.1.9

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.
Files changed (44) hide show
  1. data/History.txt +5 -0
  2. data/Manifest.txt +22 -11
  3. data/README.txt +2 -2
  4. data/Rakefile.hoe +2 -1
  5. data/design-notes/graphical-tests-for-rails-objects.graffle +644 -0
  6. data/examples/objects with notes.expected +5 -0
  7. data/examples/objects with notes.graffle +338 -0
  8. data/examples/objects with notes.rb +42 -0
  9. data/examples/rails-workflow-test.expected +1 -1
  10. data/examples/rails-workflow-test.graffle/data.plist +86 -7
  11. data/examples/rails-workflow-test.rb +11 -9
  12. data/graffle.tmproj +82 -190
  13. data/lib/graffle.rb +19 -2
  14. data/lib/graffle/point.rb +1 -2
  15. data/lib/graffle/stereotypes.rb +62 -16
  16. data/lib/graffle/version.rb +1 -1
  17. data/lib/graphical_tests_for_rails.rb +8 -5
  18. data/lib/graphical_tests_for_rails/graphic-volunteers.rb +75 -0
  19. data/lib/graphical_tests_for_rails/orderings.rb +90 -84
  20. data/lib/graphical_tests_for_rails/picture-appliers.rb +225 -0
  21. data/lib/graphical_tests_for_rails/text-appliers.rb +135 -0
  22. data/lib/graphical_tests_for_rails/volunteer-pool.rb +115 -0
  23. data/test/abstract-graphic-tests.rb +48 -0
  24. data/test/document-tests.rb +5 -5
  25. data/test/examples-tests.rb +42 -0
  26. data/test/graphical_tests_for_rails/{graphic-interpreter-tests.rb → deprecated-graphic-interpreter-tests.rb} +11 -21
  27. data/test/graphical_tests_for_rails/graphic-volunteer-tests.rb +218 -0
  28. data/test/graphical_tests_for_rails/in-workflow-order-tests.rb +1 -1
  29. data/test/graphical_tests_for_rails/picture-applier-tests.rb +215 -0
  30. data/test/graphical_tests_for_rails/{text-interpreter-tests.rb → text-applier-tests.rb} +17 -3
  31. data/test/graphical_tests_for_rails/util.rb +16 -0
  32. data/test/line-graphic-tests.rb +9 -1
  33. data/test/note-tests.rb +62 -0
  34. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/data.plist +0 -0
  35. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image1.png +0 -0
  36. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image2.png +0 -0
  37. data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image3.png +0 -0
  38. data/test/{graffle-file-types → sample-files}/multiple-canvases.graffle +0 -0
  39. data/test/{graffle-file-types → sample-files}/opening-tests.rb +9 -4
  40. data/test/{graffle-file-types → sample-files}/two-boxes-and-a-line.graffle +0 -0
  41. data/test/shaped-graphic-tests.rb +2 -3
  42. metadata +42 -18
  43. data/lib/graphical_tests_for_rails/interpreters.rb +0 -147
  44. data/test/tests-of-examples/workflow-slowtests.rb +0 -19
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'enumerator'
7
+ require 'test/unit/assertionfailederror'
8
+ require 'fluid'
9
+ require 'graphical_tests_for_rails/text-appliers'
10
+ require 'graphical_tests_for_rails/volunteer-pool'
11
+
12
+
13
+ module GraphicalTestsForRails
14
+
15
+ # A PictureApplier takes an array of graphics and arranges for
16
+ # messages derived from them to be sent to a _target_. PictureAppliers
17
+ # are "programmed" with other objects that handle the details of
18
+ # transforming graphics and the text they contain into messages.
19
+ #
20
+ # The
21
+ # graphics are processed in order. GraphicOrderer objects should be
22
+ # used to arrange the array before it's given to PictureApplier.
23
+ class PictureApplier
24
+ include Graffle
25
+ include GraphicalTestsForRails::TextAppliers
26
+
27
+ # The block tells the PictureApplier what to do by sending given,
28
+ # matching, use, and skip messages. That looks like this:
29
+ #
30
+ # PictureApplier.new {
31
+ # given('note').matching('ignore').skip
32
+ # given('shaped graphic content').use(ArgsFromQuotedText.new)
33
+ # }
34
+ #
35
+ # Each declaration sees if it can match a graphical object. If so,
36
+ # the last action is performed. Declarations are processed in
37
+ # order, so one match can be given precedence. Consider this:
38
+ #
39
+ # PictureApplier.new {
40
+ # given('note').matching('ignore').skip
41
+ # given('shaped graphic content').use(ArgsFromQuotedText.new)
42
+ # }
43
+ #
44
+ # This means that shaped graphic objects irrelevant to the test
45
+ # can be ignored by attaching the appropriate annotation.
46
+
47
+ def initialize(&block)
48
+ @universe = GraphicVolunteerPool.new
49
+ @mobilized = []
50
+ instance_eval(&block) if block
51
+ @mobilized << EagerButUselessVolunteer.new
52
+ end
53
+
54
+ # Claim that the PictureApplier matches graphics matching
55
+ # the _form_description_, a string. Here are the form descriptions
56
+ # currently supported:
57
+ #
58
+ # * 'shaped graphic content': A shaped graphic is a rectangle, oval,
59
+ # embedded picture, etc.: anything that is neither a line nor a
60
+ # group. The content is the text you type in when you double-click
61
+ # on the object. When given the form description, the PictureApplier
62
+ # will match any shaped graphic that contains text.
63
+ # * 'line label content': This matches any line that has a label.
64
+ # (Note: lines can have more than one label. That's not handled.)
65
+ # * 'note': Matches any object that contains an annotation. (Annotations are
66
+ # only available in OmniGraffle Pro. They're arbitrary text
67
+ # associated with a line or shaped graphic. You can see the
68
+ # annotations if you hover over the object.)
69
+ #
70
+ # The _form_description_ describes the first of two matching steps.
71
+ # If it matches, it extracts the appropriate text (content or
72
+ # annotation) and hands it to the next step, described by given.
73
+ #
74
+ # This method returns self so that other methods can be chained onto it.
75
+ def given(form_description)
76
+ @mobilized << @universe.mobilize_form_watcher(form_description)
77
+ self
78
+ end
79
+
80
+ # Claim that the PictureApplier matches particular text extracted from
81
+ # a graphical object. These _text_descriptions_ are currently supported:
82
+ #
83
+ # * 'text...': match if the text ends in ellipses (ignoring whitespace).
84
+ # * 'ignore': match if the text ends in the word "ignore" (ignoring whitespace).
85
+ #
86
+ # This method returns self so that other methods can be chained onto it.
87
+ def matching(text_description)
88
+ @mobilized.last.next = @universe.mobilize_content_watcher(text_description)
89
+ self
90
+ end
91
+
92
+ # When a PictureApplier matches an object, the extracted text is
93
+ # passed to the _text_applier_ to be turned into messages.
94
+ def use(text_applier)
95
+ @mobilized.last.text_applier = text_applier
96
+ end
97
+
98
+ # After a PictureApplier matches a graphical object, it's ignored.
99
+ # This is used to prevent text associated with the object from being
100
+ # turned into messages.
101
+ def skip
102
+ use(TextApplierThatDoesNothing.new)
103
+ end
104
+
105
+ # After the PictureApplier has been programmed, this method
106
+ # applies it to the _graphics_, resulting in methods that are sent
107
+ # to the _target_.
108
+ #
109
+ # If there's a problem, a log of what was sent to the _target_ is
110
+ # included in the exception message. The _log_ can be started out with
111
+ # the third argument, in which case new entries are appended. This can
112
+ # be useful when a single test is made up of several graphics lists.
113
+ def apply(graphics, target, log=[])
114
+ while_logging_for_test_errors(log) do
115
+ graphics.each { |g| find_volunteer_for(g).apply(g, target) }
116
+ end
117
+ end
118
+
119
+ # Given a _graphic_ find a GraphicVolunteer that can apply it.
120
+ # (Probably not for external use.)
121
+ def find_volunteer_for(graphic)
122
+ @mobilized.find { | m | m.likes?(graphic) }
123
+ end
124
+
125
+ private
126
+
127
+ def while_logging_for_test_errors(log)
128
+ Fluid.let(:log, log) do
129
+ begin
130
+ yield
131
+ rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
132
+ new_message = %Q{#{e.message} after\n#{Fluid.log.join("\n")}}
133
+ new_e = e.exception(new_message)
134
+ new_e.set_backtrace(e.backtrace)
135
+ raise new_e
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # This class is deprecated. See PictureApplier.
142
+ #
143
+ # A GraphicInterpreter takes a list of _graphics_, some
144
+ # instructions about how to interpret them, and a _target_.
145
+ # It interprets the instructions to create messages that it then
146
+ # sends to the target.
147
+ #
148
+ class GraphicInterpreter
149
+
150
+ # The _graphics_ are an array of OmniGraffle Graffle::AbstractGraphic objects. The
151
+ # _instructions_ map a string to an object that must respond to
152
+ # message_and_args_from. (See AbstractTextApplier#message_and_args_from
153
+ # for more.)
154
+ #
155
+ # Here are the available instructions (keys in _instructions_):
156
+ #
157
+ # * ['line labels' => ...]: each line within the (single)
158
+ # label on the line is to be interpreted as a message-send by an
159
+ # instance of the given class.
160
+ # * ['shaped graphic text' => ...]: each line of content
161
+ # within the graphic is to be interpreted by the given class. The
162
+ # content is the text you type in an object after double-clicking
163
+ # on it.
164
+ # * ['text...' => ...]: a line ending in ellipses is treated this way,
165
+ # no matter what other instructions say.
166
+ #
167
+ def initialize(graphics, instructions = {})
168
+ puts "Warning: #{self.class} is deprecated. Use PictureApplier instead."
169
+ @graphics = graphics
170
+ @line_label_strategy =
171
+ instructions['line labels']
172
+ @shaped_graphic_text_strategy =
173
+ instructions['shaped graphic text']
174
+
175
+ @ellipses_override = instructions['text...']
176
+ end
177
+
178
+ # Apply the _graphics_, according to the _instructions_, against
179
+ # the _target_.
180
+ def run_against(target)
181
+ @target = target
182
+ @log = []
183
+ @graphics.each do |g|
184
+ case g.stereotype.basename
185
+ when "ShapedGraphic"
186
+ process_commands(@shaped_graphic_text_strategy,
187
+ g.content.as_lines)
188
+ when "LineGraphic"
189
+ process_commands(@line_label_strategy,
190
+ g.label_lines)
191
+ end
192
+ end
193
+ end
194
+
195
+ private
196
+
197
+ def process_commands(text_applier, command_lines)
198
+ return if text_applier.nil?
199
+ command_lines.each do |line|
200
+ info = if /\.\.\.\s*$/ =~ line && @ellipses_override
201
+ @ellipses_override.message_and_args_from(line)
202
+ else
203
+ text_applier.message_and_args_from(line)
204
+ end
205
+ begin
206
+ @log << readable(*info)
207
+ @target.send(*info)
208
+ rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
209
+ new_message = %Q{#{e.message} after\n#{@log.join("\n")}}
210
+ new_e = e.exception(new_message)
211
+ new_e.set_backtrace(e.backtrace)
212
+ raise new_e
213
+ end
214
+ end
215
+ end
216
+
217
+ def readable(message, *args)
218
+ inspected = args.collect { |a| a.inspect }
219
+ "+ #{message}(#{inspected.join(', ')})"
220
+ end
221
+
222
+ end
223
+
224
+ end
225
+
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ module GraphicalTestsForRails
7
+
8
+ # Just a wrapper module so that classes are grouped nicely in rdoc.
9
+ # You don't have to include this - that happens if you require
10
+ # graphical_tests_for_rails.rb.
11
+ module TextAppliers
12
+
13
+ # This is the base class for objects that know how to convert text
14
+ # into a series of messages and send them to some target.
15
+ class AbstractTextApplier
16
+
17
+ # apply is given an array of text (_lines_). It is to convert
18
+ # that text into messages and send them to _target_.
19
+ #
20
+ def apply(lines, target)
21
+ message_sends = lines.collect {|line| message_and_args_from(line) }
22
+ message_sends.each do |m|
23
+ Fluid.log << readable(*m)
24
+ target.send(*m)
25
+ end
26
+ end
27
+
28
+ # message_and_args_from is given a string that is derived from some
29
+ # text. It must answer an array whose first element is a
30
+ # message name and whose remaining elements are appropriate message
31
+ # arguments. Must be defined in any subclass.
32
+ def message_and_args_from; subclass_responsibility; end
33
+
34
+ private
35
+ def readable(message, *args)
36
+ inspected = args.collect { |a| a.inspect }
37
+ "+ #{message}(#{inspected.join(', ')})"
38
+ end
39
+
40
+
41
+ end
42
+
43
+
44
+ # The arguments to apply are quoted within the _string_.
45
+ # The message is the text before the first arg, snake-cased.
46
+ # For example, this:
47
+ # paul logs in with "john", password "j"
48
+ # means this:
49
+ # target.paul_logs_in_with("john", "j")
50
+ #
51
+ class ArgsFromQuotedText < AbstractTextApplier
52
+
53
+ def message_and_args_from(string)
54
+ quote_marker = '%<<<==-'
55
+
56
+ string = string.gsub(/(\w)'(\w)/, "#{quote_marker}\\1\\2#{quote_marker}")
57
+ tokens = string.split(/['"],?/, allow_trailing_null_fields = -1)
58
+ message = tokens[0].strip.downcase
59
+ args = tokens[1..-1]
60
+
61
+
62
+ message.gsub!(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1\\2")
63
+ message.gsub!(/\s+/,'_')
64
+ message.gsub!(/-/, '_')
65
+ message.gsub!(/\W/, '')
66
+
67
+
68
+ args = args.enum_slice(2).collect { |e| e[0] }
69
+ args = args.collect do | arg |
70
+ arg.gsub(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1'\\2")
71
+ end
72
+
73
+ [message] + args
74
+ end
75
+ end
76
+
77
+ # Like ArgsFromQuotedText, but the first word (typically a name)
78
+ # is downcased and used as the first argument.
79
+ # For example, this:
80
+ # paul logs in with "john", password "j"
81
+ # means this:
82
+ # target.logs_in_with("paul", "john", "j")
83
+ #
84
+
85
+ class PrefixNameAndArgsFromQuotedText < ArgsFromQuotedText
86
+ def message_and_args_from(string)
87
+ user_claims(string =~ /^\s*(\w+)\s+(.*)$/) {
88
+ "'#{string}' cannot be split into a name and an action."
89
+ }
90
+ name = $1.downcase
91
+ rest = super($2)
92
+ message = rest.shift
93
+ [message, name, *rest]
94
+ end
95
+
96
+ end
97
+
98
+
99
+ # This AbstractTextApplier is created with the name of a message.
100
+ # When handed a string to apply, it downcases and strips the string,
101
+ # then uses it as the single argument to the method. For example, this:
102
+ # applier = TextAsNameOfPage.new("assert_on")
103
+ # applier.apply("HOME")
104
+ # turns into this:
105
+ # applier.assert_on("home")
106
+ class TextAsNameOfPage < AbstractTextApplier
107
+
108
+ # When producing a method call, objects of this class will
109
+ # prepend the _message_ to
110
+ # a single argument it constructs.
111
+ def initialize(message)
112
+ @message = message
113
+ end
114
+
115
+ # All of the text in the _string_ is expected to be the name
116
+ # of a page. It is downcased and treated as the single argument to
117
+ # the message passed to +new+.
118
+ def message_and_args_from(string)
119
+ [@message, string.downcase.strip]
120
+ end
121
+ end
122
+
123
+ class TextApplierThatDoesNothing # :nodoc:
124
+ def message_and_args_from(string)
125
+ [:object_id] # no_op
126
+ end
127
+
128
+ def apply(lines, target)
129
+ # do nothing, don't even bother sending a no-op
130
+ end
131
+ end
132
+
133
+ end
134
+
135
+ end
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-07-21.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ require 'graffle'
7
+ require 'graphical_tests_for_rails/graphic-volunteers'
8
+
9
+ # TODO: Turn these all into one class.
10
+ module GraphicalTestsForRails
11
+
12
+ # A GraphicVolunteerPool describes the ways a PictureApplier
13
+ # can by programmed with PictureApplier#given statements.
14
+ # You only care about this if you want to add new ways to use
15
+ # Graffle pictures in tests.
16
+
17
+ class GraphicVolunteerPool
18
+ def initialize
19
+ @graphic_maker = FormFocusedTemplate.new
20
+ @text_maker = ContentFocusedTemplate.new
21
+ end
22
+
23
+ def mobilize_form_watcher(description)
24
+ @graphic_maker.make_applier(description)
25
+ end
26
+
27
+ def mobilize_content_watcher(description)
28
+ @text_maker.make_applier(description)
29
+ end
30
+
31
+ end
32
+
33
+ # There are two distinct kinds of Templates only so they
34
+ # can produce different error messages.
35
+ class EmptyTemplate # :nodoc:
36
+ include Graffle
37
+
38
+ def initialize
39
+ @templates = {}
40
+ end
41
+
42
+ def proclaim(name)
43
+ prog1(GraphicVolunteer.new(name)) { | t | @templates[name] = t }
44
+ end
45
+
46
+ def make_applier(name)
47
+ t = @templates[name]
48
+ user_claims(t) {
49
+ "#{name} must be one of #{@templates.keys.inspect}"
50
+ }
51
+ t.dup
52
+ end
53
+ end
54
+
55
+ class FormFocusedTemplate < EmptyTemplate # :nodoc:
56
+ def initialize
57
+ super
58
+
59
+ proclaim('shaped graphic content').handles { | graphic |
60
+ shaped_graphic_with_content?(graphic)
61
+ }.and_extracts { | graphic |
62
+ graphic.content.as_lines
63
+ }
64
+
65
+ proclaim('line label content').handles { | graphic |
66
+ line_graphic_with_label_content?(graphic)
67
+ }.and_extracts { | graphic |
68
+ graphic.label_lines
69
+ }
70
+
71
+ proclaim('note').handles { | graphic |
72
+ graphic_with_note?(graphic)
73
+ }.and_extracts { | graphic |
74
+ graphic_note(graphic)
75
+ }
76
+ end
77
+
78
+ private
79
+ def labeled_line?(graphic)
80
+ return false unless graphic.behaves_like?(LineGraphic)
81
+ graphic.has_label?
82
+ end
83
+ def shaped_graphic_with_content?(graphic)
84
+ graphic.behaves_like?(ShapedGraphic) && graphic.has_content?
85
+ end
86
+
87
+ def line_graphic_with_label_content?(graphic)
88
+ labeled_line?(graphic) && graphic.label.has_content?
89
+ end
90
+
91
+ def graphic_with_note?(graphic)
92
+ return true if graphic.behaves_like?(AbstractGraphic) && graphic.has_note?
93
+ labeled_line?(graphic) && graphic.label.has_note?
94
+ end
95
+
96
+ def graphic_note(graphic)
97
+ return graphic.note.as_lines if graphic.has_note?
98
+ return graphic.label.note.as_lines
99
+ end
100
+ end
101
+
102
+ class ContentFocusedTemplate < EmptyTemplate # :nodoc:
103
+ def initialize
104
+ super
105
+
106
+ proclaim('text...').handles { | lines |
107
+ lines.join.strip =~ /\.\.+$/
108
+ }
109
+
110
+ proclaim('ignore').handles do | lines |
111
+ lines.any? { |l| l.strip.ends_with?("ignore") }
112
+ end
113
+ end
114
+ end
115
+ end