graphomaton 0.1.1 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +49 -4
- data/lib/graphomaton/exporters/dot.rb +47 -0
- data/lib/graphomaton/exporters/mermaid.rb +94 -0
- data/lib/graphomaton/exporters/plantuml.rb +47 -0
- data/lib/graphomaton/exporters/svg.rb +328 -0
- data/lib/graphomaton/exporters.rb +6 -0
- data/lib/graphomaton/version.rb +1 -1
- data/lib/graphomaton.rb +22 -212
- data/sample/basic.rb +19 -8
- data/sample/complex.rb +9 -1
- data/sample/long_names.rb +20 -0
- data/sample/nfa.rb +9 -0
- data/sample/skip_states.rb +23 -0
- data/spec/exporters/dot_spec.rb +146 -0
- data/spec/exporters/mermaid_spec.rb +154 -0
- data/spec/exporters/plantuml_spec.rb +144 -0
- data/spec/exporters/svg_spec.rb +314 -0
- data/spec/graphomaton_edge_cases_spec.rb +322 -0
- data/spec/graphomaton_spec.rb +3 -3
- metadata +18 -6
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'graphomaton'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Graphomaton::Exporters::Plantuml do
|
|
6
|
+
let(:automaton) { Graphomaton.new }
|
|
7
|
+
let(:plantuml_exporter) { described_class.new(automaton) }
|
|
8
|
+
|
|
9
|
+
describe '#export' do
|
|
10
|
+
context 'with empty automaton' do
|
|
11
|
+
it 'generates valid PlantUML syntax' do
|
|
12
|
+
plantuml_output = plantuml_exporter.export
|
|
13
|
+
expect(plantuml_output).to start_with('@startuml')
|
|
14
|
+
expect(plantuml_output).to end_with('@enduml')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'with states' do
|
|
19
|
+
before do
|
|
20
|
+
automaton.add_state('A')
|
|
21
|
+
automaton.add_state('B')
|
|
22
|
+
automaton.add_state('C')
|
|
23
|
+
automaton.add_transition('A', 'B', 'x')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'includes states in transitions' do
|
|
27
|
+
plantuml_output = plantuml_exporter.export
|
|
28
|
+
expect(plantuml_output).to include('A')
|
|
29
|
+
expect(plantuml_output).to include('B')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'marks final states' do
|
|
33
|
+
automaton.add_final('C')
|
|
34
|
+
plantuml_output = plantuml_exporter.export
|
|
35
|
+
expect(plantuml_output).to match(/C\s+-->\s+\[\*\]/)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'with initial state' do
|
|
40
|
+
before do
|
|
41
|
+
automaton.add_state('Start')
|
|
42
|
+
automaton.set_initial('Start')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'marks initial state with arrow from start' do
|
|
46
|
+
plantuml_output = plantuml_exporter.export
|
|
47
|
+
expect(plantuml_output).to include('[*] --> Start')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'with transitions' do
|
|
52
|
+
before do
|
|
53
|
+
automaton.add_state('A')
|
|
54
|
+
automaton.add_state('B')
|
|
55
|
+
automaton.add_state('C')
|
|
56
|
+
automaton.add_transition('A', 'B', 'input_a')
|
|
57
|
+
automaton.add_transition('B', 'C', 'input_b')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'includes all transitions with labels' do
|
|
61
|
+
plantuml_output = plantuml_exporter.export
|
|
62
|
+
expect(plantuml_output).to match(/A\s+-->\s+B\s*:\s*input_a/)
|
|
63
|
+
expect(plantuml_output).to match(/B\s+-->\s+C\s*:\s*input_b/)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'handles self-loops' do
|
|
67
|
+
automaton.add_transition('B', 'B', 'loop')
|
|
68
|
+
plantuml_output = plantuml_exporter.export
|
|
69
|
+
expect(plantuml_output).to match(/B\s+-->\s+B\s*:\s*loop/)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context 'with special characters' do
|
|
74
|
+
before do
|
|
75
|
+
automaton.add_state('State 1')
|
|
76
|
+
automaton.add_state('State-2')
|
|
77
|
+
automaton.add_transition('State 1', 'State-2', 'a/b')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'handles spaces in state names' do
|
|
81
|
+
plantuml_output = plantuml_exporter.export
|
|
82
|
+
expect(plantuml_output).to include('State_1')
|
|
83
|
+
expect(plantuml_output).to include('State_2')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'handles special characters in labels' do
|
|
87
|
+
plantuml_output = plantuml_exporter.export
|
|
88
|
+
expect(plantuml_output).to include('a/b')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'with complete automaton' do
|
|
93
|
+
before do
|
|
94
|
+
automaton.add_state('q0')
|
|
95
|
+
automaton.add_state('q1')
|
|
96
|
+
automaton.add_state('q2')
|
|
97
|
+
automaton.set_initial('q0')
|
|
98
|
+
automaton.add_final('q2')
|
|
99
|
+
automaton.add_transition('q0', 'q1', 'a')
|
|
100
|
+
automaton.add_transition('q1', 'q2', 'b')
|
|
101
|
+
automaton.add_transition('q2', 'q0', 'c')
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'generates complete valid PlantUML diagram' do
|
|
105
|
+
plantuml_output = plantuml_exporter.export
|
|
106
|
+
|
|
107
|
+
# Should be wrapped in @startuml/@enduml
|
|
108
|
+
expect(plantuml_output).to start_with('@startuml')
|
|
109
|
+
expect(plantuml_output).to end_with('@enduml')
|
|
110
|
+
|
|
111
|
+
# Should have initial state marker
|
|
112
|
+
expect(plantuml_output).to include('[*] --> q0')
|
|
113
|
+
|
|
114
|
+
# Should have final state marker
|
|
115
|
+
expect(plantuml_output).to match(/q2\s+-->\s+\[\*\]/)
|
|
116
|
+
|
|
117
|
+
# Should have states in transitions
|
|
118
|
+
expect(plantuml_output).to include('q0')
|
|
119
|
+
expect(plantuml_output).to include('q1')
|
|
120
|
+
expect(plantuml_output).to include('q2')
|
|
121
|
+
|
|
122
|
+
# Should have all transitions
|
|
123
|
+
expect(plantuml_output).to match(/q0\s+-->\s+q1\s*:\s*a/)
|
|
124
|
+
expect(plantuml_output).to match(/q1\s+-->\s+q2\s*:\s*b/)
|
|
125
|
+
expect(plantuml_output).to match(/q2\s+-->\s+q0\s*:\s*c/)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context 'with non-ASCII characters' do
|
|
130
|
+
before do
|
|
131
|
+
automaton.add_state('状態A')
|
|
132
|
+
automaton.add_state('状態B')
|
|
133
|
+
automaton.add_transition('状態A', '状態B', '遷移')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'handles Japanese characters correctly' do
|
|
137
|
+
plantuml_output = plantuml_exporter.export
|
|
138
|
+
expect(plantuml_output).to include('状態A')
|
|
139
|
+
expect(plantuml_output).to include('状態B')
|
|
140
|
+
expect(plantuml_output).to include('遷移')
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'graphomaton'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Graphomaton::Exporters::Svg do
|
|
6
|
+
let(:automaton) { Graphomaton.new }
|
|
7
|
+
let(:svg_exporter) { described_class.new(automaton) }
|
|
8
|
+
|
|
9
|
+
describe '#initialize' do
|
|
10
|
+
it 'initializes with an automaton' do
|
|
11
|
+
expect(svg_exporter).to be_a(described_class)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#export' do
|
|
16
|
+
context 'with empty automaton' do
|
|
17
|
+
it 'generates valid SVG' do
|
|
18
|
+
svg_output = svg_exporter.export
|
|
19
|
+
expect { REXML::Document.new(svg_output) }.not_to raise_error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'uses default dimensions' do
|
|
23
|
+
svg_output = svg_exporter.export
|
|
24
|
+
doc = REXML::Document.new(svg_output)
|
|
25
|
+
svg = doc.root
|
|
26
|
+
expect(svg.attributes['width']).to eq('800')
|
|
27
|
+
expect(svg.attributes['height']).to eq('600')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'accepts custom dimensions' do
|
|
31
|
+
svg_output = svg_exporter.export(1000, 800)
|
|
32
|
+
doc = REXML::Document.new(svg_output)
|
|
33
|
+
svg = doc.root
|
|
34
|
+
expect(svg.attributes['width']).to eq('1000')
|
|
35
|
+
expect(svg.attributes['height']).to eq('800')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context 'with states and transitions' do
|
|
40
|
+
before do
|
|
41
|
+
automaton.add_state('A')
|
|
42
|
+
automaton.add_state('B')
|
|
43
|
+
automaton.add_state('C')
|
|
44
|
+
automaton.set_initial('A')
|
|
45
|
+
automaton.add_final('C')
|
|
46
|
+
automaton.add_transition('A', 'B', 'a')
|
|
47
|
+
automaton.add_transition('B', 'C', 'b')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'includes all states as circles' do
|
|
51
|
+
svg_output = svg_exporter.export
|
|
52
|
+
doc = REXML::Document.new(svg_output)
|
|
53
|
+
circles = REXML::XPath.match(doc, '//circle[@class="state-circle" or contains(@class, "final-state")]')
|
|
54
|
+
# Should have at least 3 circles (one per state, plus inner circle for final state)
|
|
55
|
+
expect(circles.size).to be >= 3
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'includes state labels' do
|
|
59
|
+
svg_output = svg_exporter.export
|
|
60
|
+
doc = REXML::Document.new(svg_output)
|
|
61
|
+
labels = REXML::XPath.match(doc, '//text[@class="state-text"]')
|
|
62
|
+
label_texts = labels.map(&:text)
|
|
63
|
+
expect(label_texts).to include('A', 'B', 'C')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'includes transition labels' do
|
|
67
|
+
svg_output = svg_exporter.export
|
|
68
|
+
doc = REXML::Document.new(svg_output)
|
|
69
|
+
labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
|
|
70
|
+
label_texts = labels.map(&:text)
|
|
71
|
+
expect(label_texts).to include('a', 'b')
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with skip states transitions' do
|
|
76
|
+
before do
|
|
77
|
+
automaton.add_state('A')
|
|
78
|
+
automaton.add_state('B')
|
|
79
|
+
automaton.add_state('C')
|
|
80
|
+
automaton.add_state('D')
|
|
81
|
+
automaton.set_initial('A')
|
|
82
|
+
automaton.add_final('D')
|
|
83
|
+
automaton.add_transition('A', 'B', '1 step')
|
|
84
|
+
automaton.add_transition('A', 'C', 'skip 1 state')
|
|
85
|
+
automaton.add_transition('A', 'D', 'skip 2 states')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'creates curved paths for skip transitions' do
|
|
89
|
+
svg_output = svg_exporter.export
|
|
90
|
+
doc = REXML::Document.new(svg_output)
|
|
91
|
+
paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
|
|
92
|
+
# A->C and A->D should be curved (quadratic bezier)
|
|
93
|
+
curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
|
|
94
|
+
expect(curved_paths.size).to be >= 2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'creates different curve heights for multiple skip transitions' do
|
|
98
|
+
svg_output = svg_exporter.export
|
|
99
|
+
doc = REXML::Document.new(svg_output)
|
|
100
|
+
paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
|
|
101
|
+
curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
|
|
102
|
+
|
|
103
|
+
# Extract control points (y-coordinates) from path data
|
|
104
|
+
control_points = curved_paths.map do |path|
|
|
105
|
+
d = path.attributes['d']
|
|
106
|
+
# Parse "M x1 y1 Q cx cy, x2 y2" format
|
|
107
|
+
match = d.match(/Q\s+([\d.]+)\s+([\d.-]+)/)
|
|
108
|
+
match ? match[2].to_f : nil
|
|
109
|
+
end.compact
|
|
110
|
+
|
|
111
|
+
# Control points should be different (transitions shouldn't overlap)
|
|
112
|
+
expect(control_points.uniq.size).to eq(control_points.size)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'includes all transition labels with proper positioning' do
|
|
116
|
+
svg_output = svg_exporter.export
|
|
117
|
+
doc = REXML::Document.new(svg_output)
|
|
118
|
+
labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
|
|
119
|
+
label_texts = labels.map(&:text).reject { |t| t == 'start' }
|
|
120
|
+
expect(label_texts).to include('1 step', 'skip 1 state', 'skip 2 states')
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context 'with self-loop transitions' do
|
|
125
|
+
before do
|
|
126
|
+
automaton.add_state('A')
|
|
127
|
+
automaton.add_transition('A', 'A', 'loop')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'creates cubic bezier curve for self-loop' do
|
|
131
|
+
svg_output = svg_exporter.export
|
|
132
|
+
doc = REXML::Document.new(svg_output)
|
|
133
|
+
paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
|
|
134
|
+
self_loop = paths.find { |p| p.attributes['d'].include?('C') }
|
|
135
|
+
expect(self_loop).not_to be_nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'positions label above the loop' do
|
|
139
|
+
svg_output = svg_exporter.export
|
|
140
|
+
doc = REXML::Document.new(svg_output)
|
|
141
|
+
label = REXML::XPath.first(doc, '//text[@class="transition-label" and text()="loop"]')
|
|
142
|
+
expect(label).not_to be_nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'with long state names' do
|
|
147
|
+
before do
|
|
148
|
+
automaton.add_state('VeryLongStateName')
|
|
149
|
+
automaton.add_state('AnotherLongName')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'adjusts font size for long names' do
|
|
153
|
+
svg_output = svg_exporter.export
|
|
154
|
+
doc = REXML::Document.new(svg_output)
|
|
155
|
+
texts = REXML::XPath.match(doc, '//text[@class="state-text"]')
|
|
156
|
+
|
|
157
|
+
# At least one text should have reduced font size
|
|
158
|
+
font_sizes = texts.map { |t| t.attributes['font-size'].to_i }
|
|
159
|
+
expect(font_sizes).to include(satisfy { |size| size < 20 })
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'renders all state names' do
|
|
163
|
+
svg_output = svg_exporter.export
|
|
164
|
+
doc = REXML::Document.new(svg_output)
|
|
165
|
+
labels = REXML::XPath.match(doc, '//text[@class="state-text"]')
|
|
166
|
+
label_texts = labels.map(&:text)
|
|
167
|
+
expect(label_texts).to include('VeryLongStateName', 'AnotherLongName')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context 'with non-ASCII characters' do
|
|
172
|
+
before do
|
|
173
|
+
automaton.add_state('状態A')
|
|
174
|
+
automaton.add_state('状態B')
|
|
175
|
+
automaton.add_transition('状態A', '状態B', '遷移')
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it 'handles Japanese characters correctly' do
|
|
179
|
+
svg_output = svg_exporter.export
|
|
180
|
+
doc = REXML::Document.new(svg_output)
|
|
181
|
+
labels = REXML::XPath.match(doc, '//text[@class="state-text"]')
|
|
182
|
+
label_texts = labels.map(&:text)
|
|
183
|
+
expect(label_texts).to include('状態A', '状態B')
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'calculates proper text width for non-ASCII labels' do
|
|
187
|
+
svg_output = svg_exporter.export
|
|
188
|
+
doc = REXML::Document.new(svg_output)
|
|
189
|
+
label_bg = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
|
|
190
|
+
|
|
191
|
+
# Background rectangles should have appropriate widths
|
|
192
|
+
widths = label_bg.map { |rect| rect.attributes['width'].to_f }
|
|
193
|
+
expect(widths).to all(be > 0)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context 'with parallel transitions' do
|
|
198
|
+
before do
|
|
199
|
+
automaton.add_state('A')
|
|
200
|
+
automaton.add_state('B')
|
|
201
|
+
automaton.add_state('C')
|
|
202
|
+
automaton.add_transition('A', 'C', 'skip forward')
|
|
203
|
+
automaton.add_transition('C', 'A', 'skip backward')
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'creates curved paths for bidirectional transitions' do
|
|
207
|
+
svg_output = svg_exporter.export
|
|
208
|
+
doc = REXML::Document.new(svg_output)
|
|
209
|
+
paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
|
|
210
|
+
curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
|
|
211
|
+
expect(curved_paths.size).to be >= 2
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it 'offsets parallel transitions to avoid overlap' do
|
|
215
|
+
svg_output = svg_exporter.export
|
|
216
|
+
doc = REXML::Document.new(svg_output)
|
|
217
|
+
paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
|
|
218
|
+
|
|
219
|
+
# Paths should have different control points
|
|
220
|
+
control_points = paths.map do |path|
|
|
221
|
+
d = path.attributes['d']
|
|
222
|
+
match = d.match(/Q\s+([\d.]+)\s+([\d.-]+)/)
|
|
223
|
+
match ? [match[1].to_f, match[2].to_f] : nil
|
|
224
|
+
end.compact
|
|
225
|
+
|
|
226
|
+
expect(control_points.uniq.size).to eq(control_points.size)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
describe 'text width calculation' do
|
|
232
|
+
it 'calculates width for ASCII text' do
|
|
233
|
+
width = svg_exporter.send(:calculate_text_width, 'hello')
|
|
234
|
+
expect(width).to be > 0
|
|
235
|
+
expect(width).to be < 100
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it 'calculates width for non-ASCII text' do
|
|
239
|
+
width = svg_exporter.send(:calculate_text_width, 'こんにちは')
|
|
240
|
+
expect(width).to be > 0
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'calculates width for mixed text' do
|
|
244
|
+
width = svg_exporter.send(:calculate_text_width, 'hello世界')
|
|
245
|
+
expect(width).to be > 0
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'returns minimum width for empty text' do
|
|
249
|
+
width = svg_exporter.send(:calculate_text_width, '')
|
|
250
|
+
expect(width).to eq(60)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it 'returns minimum width for very short text' do
|
|
254
|
+
width = svg_exporter.send(:calculate_text_width, 'a')
|
|
255
|
+
expect(width).to eq(60)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe 'state font size calculation' do
|
|
260
|
+
it 'returns base size for short names' do
|
|
261
|
+
size = svg_exporter.send(:calculate_state_font_size, 'A')
|
|
262
|
+
expect(size).to eq(20)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it 'reduces size for long names' do
|
|
266
|
+
size = svg_exporter.send(:calculate_state_font_size, 'VeryLongStateName')
|
|
267
|
+
expect(size).to be < 20
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it 'ensures minimum font size' do
|
|
271
|
+
size = svg_exporter.send(:calculate_state_font_size, 'SuperExtremelyLongStateNameThatWouldBeVeryHardToFit')
|
|
272
|
+
expect(size).to be >= 12
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'handles non-ASCII characters' do
|
|
276
|
+
size = svg_exporter.send(:calculate_state_font_size, '状態名前')
|
|
277
|
+
expect(size).to be > 0
|
|
278
|
+
expect(size).to be <= 20
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
describe 'label background' do
|
|
283
|
+
before do
|
|
284
|
+
automaton.add_state('A')
|
|
285
|
+
automaton.add_state('B')
|
|
286
|
+
automaton.add_transition('A', 'B', 'test label')
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'creates background rectangles for labels' do
|
|
290
|
+
svg_output = svg_exporter.export
|
|
291
|
+
doc = REXML::Document.new(svg_output)
|
|
292
|
+
backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
|
|
293
|
+
expect(backgrounds).not_to be_empty
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'positions backgrounds correctly' do
|
|
297
|
+
svg_output = svg_exporter.export
|
|
298
|
+
doc = REXML::Document.new(svg_output)
|
|
299
|
+
backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
|
|
300
|
+
|
|
301
|
+
backgrounds.each do |bg|
|
|
302
|
+
x = bg.attributes['x'].to_f
|
|
303
|
+
y = bg.attributes['y'].to_f
|
|
304
|
+
width = bg.attributes['width'].to_f
|
|
305
|
+
height = bg.attributes['height'].to_f
|
|
306
|
+
|
|
307
|
+
expect(x).to be_a(Float)
|
|
308
|
+
expect(y).to be_a(Float)
|
|
309
|
+
expect(width).to be > 0
|
|
310
|
+
expect(height).to eq(20)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|