graffle 0.1.7 → 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
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