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.
Files changed (33) hide show
  1. data/History.txt +5 -4
  2. data/Manifest.txt +13 -10
  3. data/Rakefile.hoe +1 -1
  4. data/examples/rails-workflow-test.expected +10 -0
  5. data/examples/rails-workflow-test.graffle/data.plist +1033 -0
  6. data/examples/rails-workflow-test.graffle/image1.png +0 -0
  7. data/examples/rails-workflow-test.graffle/image2.png +0 -0
  8. data/examples/rails-workflow-test.graffle/image3.png +0 -0
  9. data/examples/rails-workflow-test.rb +40 -0
  10. data/graffle.tmproj +431 -2
  11. data/lib/graffle/stereotypes.rb +15 -4
  12. data/lib/graffle/styled-text-reader.rb +3 -0
  13. data/lib/graffle/version.rb +1 -1
  14. data/lib/graphical_tests_for_rails.rb +25 -0
  15. data/lib/graphical_tests_for_rails/interpreters.rb +147 -0
  16. data/lib/graphical_tests_for_rails/orderings.rb +101 -0
  17. data/test/graffle-file-types/opening-tests.rb +0 -1
  18. data/test/graphical_tests_for_rails/graphic-interpreter-tests.rb +131 -0
  19. data/test/graphical_tests_for_rails/in-workflow-order-tests.rb +165 -0
  20. data/test/{grails-tests/commands-from-quoted-args-tests.rb → graphical_tests_for_rails/text-interpreter-tests.rb} +38 -27
  21. data/test/line-graphic-tests.rb +8 -0
  22. data/test/sheet-tests.rb +18 -0
  23. data/test/tests-of-examples/workflow-slowtests.rb +19 -0
  24. metadata +20 -18
  25. data/bin/bin-skeleton +0 -13
  26. data/lib/graffle/lib-skeleton +0 -3
  27. data/lib/grail_test.rb +0 -16
  28. data/lib/grail_test/command-interpreters.rb +0 -78
  29. data/test/grails-tests/destinations-tests.rb +0 -19
  30. data/test/grails-tests/do-nothing-commands-tests.rb +0 -18
  31. data/test/grails-tests/graphic-interpreter-tests.rb +0 -70
  32. data/test/grails-tests/translation-testcase.rb +0 -25
  33. data/test/test-skeleton +0 -19
@@ -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
- container.find_by_id(self[which]['ID'])
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
 
@@ -4,5 +4,5 @@
4
4
  # Copyright (c) 2007. All rights reserved.
5
5
 
6
6
  module Graffle
7
- Version = '0.1.7'
7
+ Version = '0.1.8'
8
8
  end
@@ -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