cml 1.4.2 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +33 -0
  4. data/README.rdoc +13 -3
  5. data/Rakefile +9 -49
  6. data/cml.gemspec +23 -125
  7. data/lib/cml/converters/jsawesome.rb +3 -3
  8. data/lib/cml/gold.rb +12 -7
  9. data/lib/cml/liquid_filters.rb +4 -0
  10. data/lib/cml/logic.rb +424 -0
  11. data/lib/cml/logic_tree/graph.rb +107 -0
  12. data/lib/cml/logic_tree/solver.rb +43 -0
  13. data/lib/cml/parser.rb +47 -7
  14. data/lib/cml/tag.rb +42 -21
  15. data/lib/cml/tags/checkbox.rb +14 -4
  16. data/lib/cml/tags/checkboxes.rb +4 -0
  17. data/lib/cml/tags/group.rb +4 -0
  18. data/lib/cml/tags/hours.rb +263 -0
  19. data/lib/cml/tags/iterate.rb +4 -0
  20. data/lib/cml/tags/meta.rb +4 -0
  21. data/lib/cml/tags/option.rb +4 -0
  22. data/lib/cml/tags/radio.rb +13 -4
  23. data/lib/cml/tags/radios.rb +4 -0
  24. data/lib/cml/tags/ratings.rb +9 -1
  25. data/lib/cml/tags/search.rb +8 -2
  26. data/lib/cml/tags/select.rb +6 -2
  27. data/lib/cml/tags/taxonomy.rb +148 -0
  28. data/lib/cml/tags/thumb.rb +4 -0
  29. data/lib/cml/tags/unknown.rb +4 -0
  30. data/lib/cml/version.rb +3 -0
  31. data/lib/cml.rb +3 -0
  32. data/spec/converters/jsawesome_spec.rb +0 -9
  33. data/spec/fixtures/logic_broken.cml +5 -0
  34. data/spec/fixtures/logic_circular.cml +5 -0
  35. data/spec/fixtures/logic_grouped.cml +7 -0
  36. data/spec/fixtures/logic_nested.cml +7 -0
  37. data/spec/fixtures/logic_none.cml +7 -0
  38. data/spec/fixtures/logic_not_nested.cml +7 -0
  39. data/spec/liquid_filter_spec.rb +19 -0
  40. data/spec/logic_depends_on_spec.rb +242 -0
  41. data/spec/logic_spec.rb +207 -0
  42. data/spec/logic_tree_graph_spec.rb +465 -0
  43. data/spec/logic_tree_solver_spec.rb +58 -0
  44. data/spec/meta_spec.rb +12 -2
  45. data/spec/show_data_spec.rb +3 -2
  46. data/spec/spec_helper.rb +22 -6
  47. data/spec/tags/checkboxes_spec.rb +2 -2
  48. data/spec/tags/group_spec.rb +5 -5
  49. data/spec/tags/hours_spec.rb +404 -0
  50. data/spec/tags/radios_spec.rb +2 -2
  51. data/spec/tags/ratings_spec.rb +1 -1
  52. data/spec/tags/select_spec.rb +45 -0
  53. data/spec/tags/tag_spec.rb +25 -0
  54. data/spec/tags/taxonomy_spec.rb +112 -0
  55. data/spec/validation_spec.rb +52 -0
  56. metadata +112 -17
  57. data/VERSION +0 -1
@@ -0,0 +1,207 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "CML logic" do
4
+ describe "properties" do
5
+ describe "with no logic" do
6
+ before :each do
7
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_none.cml" )
8
+ @p = Parser.new( @cml )
9
+ end
10
+
11
+ it "is not nested" do
12
+ @p.logic_tree.max_depth.should == 1
13
+ @p.has_nested_logic?.should be_false
14
+ end
15
+
16
+ it "is not grouped" do
17
+ @p.has_grouped_logic?.should be_false
18
+ end
19
+ end
20
+
21
+ describe "without nested logic" do
22
+ before :each do
23
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_not_nested.cml" )
24
+ @p = Parser.new( @cml )
25
+ end
26
+
27
+ it "is not nested" do
28
+ @p.logic_tree.max_depth.should == 2
29
+ @p.has_nested_logic?.should be_false
30
+ end
31
+
32
+ it "is grouped" do
33
+ @p.has_grouped_logic?.should be_true
34
+ end
35
+ end
36
+
37
+ describe "with nested logic" do
38
+ before :each do
39
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_nested.cml" )
40
+ @p = Parser.new( @cml )
41
+ end
42
+
43
+ it "is nested" do
44
+ @p.logic_tree.max_depth.should == 4
45
+ @p.has_nested_logic?.should be_true
46
+ end
47
+
48
+ it "is not grouped" do
49
+ @p.has_grouped_logic?.should be_false
50
+ end
51
+ end
52
+
53
+ describe "with circular logic safety" do
54
+ before :each do
55
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_circular.cml" )
56
+ @p = Parser.new( @cml )
57
+ end
58
+
59
+ it "is nested" do
60
+ @p.logic_tree.max_depth.should == 10
61
+ @p.has_nested_logic?.should be_true
62
+ end
63
+
64
+ it "is not grouped" do
65
+ @p.has_grouped_logic?.should be_false
66
+ end
67
+ end
68
+
69
+ describe "with broken logic safety" do
70
+ before :each do
71
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_broken.cml" )
72
+ @p = Parser.new( @cml )
73
+ end
74
+
75
+ it "is not nested" do
76
+ @p.logic_tree.max_depth.should == 1
77
+ @p.has_nested_logic?.should be_false
78
+ end
79
+
80
+ it "is not grouped" do
81
+ @p.has_grouped_logic?.should be_false
82
+ end
83
+ end
84
+
85
+ describe "with grouped logic" do
86
+ before :each do
87
+ @cml = File.read( File.dirname(__FILE__) + "/fixtures/logic_grouped.cml" )
88
+ @p = Parser.new( @cml )
89
+ end
90
+
91
+ it "is nested" do
92
+ @p.logic_tree.max_depth.should == 4
93
+ @p.has_nested_logic?.should be_true
94
+ end
95
+
96
+ it "is grouped" do
97
+ @p.has_grouped_logic?.should be_true
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "parsing" do
103
+
104
+ describe "maps tokens and their combinators" do
105
+
106
+ it "without boolean logic" do
107
+ parsed = CML::TagLogic.parse_expression("omg:[crispy]")
108
+ parsed.should == [
109
+ ["", "omg:[crispy]"]]
110
+ end
111
+
112
+ it "with grouping and 'and' precedence logic" do
113
+ parsed = CML::TagLogic.parse_expression("omg:[crispy]||(w_t_f:[rtfm]++f_t_w:[rtfm])||!w_t_f:rtfm")
114
+ parsed.should == [
115
+ ["", "omg:[crispy]"],
116
+ ["||", [
117
+ ["", [
118
+ ["", "w_t_f:[rtfm]"], ["++", "f_t_w:[rtfm]"]]]]],
119
+ ["||", "!w_t_f:rtfm"]]
120
+ end
121
+
122
+ it "with grouping logic between 'or'" do
123
+ parsed = CML::TagLogic.parse_expression("omg:[crispy]||(w_t_f:[rtfm]||f_t_w:[rtfm])||!w_t_f:rtfm")
124
+ parsed.should == [
125
+ ["", "omg:[crispy]"],
126
+ ["||", [
127
+ ["", "w_t_f:[rtfm]"], ["||", "f_t_w:[rtfm]"]]],
128
+ ["||", "!w_t_f:rtfm"]]
129
+ end
130
+
131
+ it "with grouping logic between 'and'" do
132
+ pending "Will work with proper combinator support"
133
+ parsed = CML::TagLogic.parse_expression("omg:[crispy]||w_t_f:[rtfm]++(f_t_w:[rtfm]||!w_t_f:rtfm)")
134
+ parsed.should == [
135
+ ["", "omg:[crispy]"],
136
+ ["++", [
137
+ ["", "w_t_f:[rtfm]"],
138
+ ["||", [
139
+ ["", "f_t_w:[rtfm]"],
140
+ ["||", "!w_t_f:rtfm"]]]
141
+ ]
142
+ ]
143
+ ]
144
+ end
145
+
146
+ it "with 'and' precedence logic" do
147
+ parsed = CML::TagLogic.parse_expression("omg:[crispy]||w_t_f:[rtfm]++f_t_w:[rtfm]||!w_t_f:rtfm")
148
+ parsed.should == [
149
+ ["", "omg:[crispy]"],
150
+ ["||", [
151
+ ["", "w_t_f:[rtfm]"], ["++", "f_t_w:[rtfm]"]]],
152
+ ["||", "!w_t_f:rtfm"]]
153
+ end
154
+
155
+ end
156
+
157
+ describe "resolves combinators" do
158
+
159
+ def test_rez(exp)
160
+ parsed = CML::TagLogic.parse_expression(exp)
161
+ combinators = []
162
+ parsed.each_with_index do |p,i|
163
+ if Array===p[1]
164
+ p[1].each_with_index do |pp,ii|
165
+ combinators << CML::TagLogic.resolve_combinator(p[1], ii)
166
+ end
167
+ else
168
+ combinators << CML::TagLogic.resolve_combinator(parsed, i)
169
+ end
170
+ end
171
+ combinators
172
+ end
173
+
174
+ it "without boolean logic" do
175
+ combinators = test_rez("omg:[crispy]")
176
+ combinators.should == ["||"]
177
+ end
178
+
179
+ it "for an 'and' phrase" do
180
+ combinators = test_rez("omg:[crispy]++!w_t_f:rtfm")
181
+ combinators.should == ["&&", "&&"]
182
+ end
183
+
184
+ it "for an 'or' phrase" do
185
+ combinators = test_rez("omg:[crispy]||!w_t_f:rtfm")
186
+ combinators.should == ["||", "||"]
187
+ end
188
+
189
+ it "with grouping logic between 'or'" do
190
+ combinators = test_rez("omg:[crispy]||(w_t_f:[rtfm]||f_t_w:[rtfm])||!w_t_f:rtfm")
191
+ combinators.should == ["||", "||", "||", "||"]
192
+ end
193
+
194
+ it "with grouping logic between 'and'" do
195
+ pending "A tough nut to crack :'( "
196
+ combinators = test_rez("omg:[crispy]||w_t_f:[rtfm]++(f_t_w:[rtfm]||!w_t_f:rtfm)")
197
+ combinators.should == ["||", "&&", "&&", "||"]
198
+ end
199
+
200
+ it "with 'and' precedence logic" do
201
+ combinators = test_rez("omg:[crispy]||w_t_f:[rtfm]++f_t_w:[rtfm]||!w_t_f:rtfm")
202
+ combinators.should == ["||", "&&", "&&", "||"]
203
+ end
204
+
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,465 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "CML logic tree graph" do
4
+
5
+ it "generates for a simple only-if selector" do
6
+ @p = Parser.new(<<-HTML)
7
+ <cml:text label="OMG" />
8
+ <cml:checkboxes label="w t f" only-if="omg">
9
+ <cml:checkbox label="lol" />
10
+ <cml:checkbox label="rtfm" />
11
+ </cml:checkboxes>
12
+ HTML
13
+
14
+ graph = @p.logic_tree.graph
15
+ graph.should == {
16
+ "omg"=>{:outbound_count=>1, :outbound_names=>["w_t_f"]},
17
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]}}
18
+ }
19
+ end
20
+
21
+ it "generates for an only-if 'not' selector" do
22
+ @p = Parser.new(<<-HTML)
23
+ <cml:text label="OMG" />
24
+ <cml:checkboxes label="w t f" only-if="!omg">
25
+ <cml:checkbox label="lol" />
26
+ <cml:checkbox label="rtfm" />
27
+ </cml:checkboxes>
28
+ HTML
29
+
30
+ graph = @p.logic_tree.graph
31
+ graph.should == {
32
+ "omg"=>{:outbound_count=>1, :outbound_names=>["w_t_f"]},
33
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>false}]}]}}
34
+ }
35
+ end
36
+
37
+ it "generates for an only-if 'unchecked' selector for checkboxes" do
38
+ @p = Parser.new(<<-HTML)
39
+ <cml:checkboxes label="w t f">
40
+ <cml:checkbox label="lol" />
41
+ <cml:checkbox label="rtfm" />
42
+ </cml:checkboxes>
43
+ <cml:text label="OMG" only-if="w_t_f:unchecked" />
44
+ HTML
45
+
46
+ graph = @p.logic_tree.graph
47
+ graph.should == {
48
+ "w_t_f"=>{:outbound_count=>1, :outbound_names=>["omg"]},
49
+ "omg"=>{:inbound_count=>1, :inbound_names=>["w_t_f"], :inbound_matches=>{"||" => [{"w_t_f"=>[{:match_key => 'lol', :is_not=>true}, {:match_key => 'rtfm', :is_not=>true}]}]}}
50
+ }
51
+ end
52
+
53
+ it "generates for an only-if 'unchecked' selector for one checkbox" do
54
+ @p = Parser.new(<<-HTML)
55
+ <cml:checkbox label="w t f">
56
+ <cml:text label="OMG" only-if="w_t_f:unchecked" />
57
+ HTML
58
+
59
+ graph = @p.logic_tree.graph
60
+ graph.should == {
61
+ "w_t_f"=>{:outbound_count=>1, :outbound_names=>["omg"]},
62
+ "omg"=>{:inbound_count=>1, :inbound_names=>["w_t_f"], :inbound_matches=>{"||" => [{"w_t_f"=>[{:match_key => "true", :is_not=>true}]}]}}
63
+ }
64
+ end
65
+
66
+ it "generates for complex only-if 'and' selectors" do
67
+ @p = Parser.new(<<-HTML)
68
+ <cml:text label="OMG" />
69
+ <cml:checkboxes label="w t f" only-if="omg">
70
+ <cml:checkbox label="lol" />
71
+ <cml:checkbox label="rtfm" />
72
+ </cml:checkboxes>
73
+ <cml:radios label="bacon" only-if="omg:[crispy]++w_t_f:[lol]">
74
+ <cml:radio label="tasty" />
75
+ <cml:radio label="too meaty" />
76
+ </cml:radios>
77
+ HTML
78
+
79
+ graph = @p.logic_tree.graph
80
+ graph.should == {
81
+ "omg"=>{:outbound_names=>["bacon", "w_t_f"], :outbound_count=>2},
82
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{
83
+ "||"=>[{
84
+ "omg"=>[{:is_not=>true, :match_key=>nil}]}]}, :outbound_names=>["bacon"], :outbound_count=>1},
85
+ "bacon"=>{:inbound_count=>2, :inbound_names=>["omg", "w_t_f"], :inbound_matches=>{
86
+ "||" => [{
87
+ "&&" => [
88
+ {"omg"=>[{:is_not=>false, :match_key=>"crispy"}]},
89
+ {"w_t_f"=>[{:is_not=>false, :match_key=>"lol"}]}
90
+ ]
91
+ }]
92
+ }}
93
+ }
94
+ end
95
+
96
+ it "generates for complex only-if 'or' selectors" do
97
+ @p = Parser.new(<<-HTML)
98
+ <cml:text label="OMG" />
99
+ <cml:checkboxes label="w t f" only-if="omg">
100
+ <cml:checkbox label="lol" />
101
+ <cml:checkbox label="rtfm" />
102
+ </cml:checkboxes>
103
+ <cml:radios label="bacon" only-if="omg:[crispy]||!w_t_f:[lol]">
104
+ <cml:radio label="tasty" />
105
+ <cml:radio label="too meaty" />
106
+ </cml:radios>
107
+ HTML
108
+
109
+ graph = @p.logic_tree.graph
110
+ expected = {
111
+ "omg"=>{:outbound_names=>["bacon", "w_t_f"], :outbound_count=>2},
112
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:is_not=>true, :match_key => nil}]}]},
113
+ :outbound_count=>1, :outbound_names=>["bacon"]},
114
+ "bacon"=>{:inbound_count=>2, :inbound_names=>["omg", "w_t_f"], :inbound_matches=>{
115
+ "||" => [
116
+ {"omg"=>[{:is_not=>false, :match_key => "crispy"}]},
117
+ {"w_t_f"=>[{:is_not=>true, :match_key => "lol"}]}
118
+ ]
119
+ }}
120
+ }
121
+ graph.should == expected
122
+ end
123
+
124
+ it "generates with 'and' precedence" do
125
+ @p = Parser.new(<<-HTML)
126
+ <cml:text label="OMG" />
127
+ <cml:checkboxes label="w t f" only-if="omg">
128
+ <cml:checkbox label="lol" />
129
+ <cml:checkbox label="rtfm" />
130
+ </cml:checkboxes>
131
+ <cml:checkboxes label="f t w" only-if="omg">
132
+ <cml:checkbox label="lmao" />
133
+ <cml:checkbox label="rtfm" />
134
+ </cml:checkboxes>
135
+ <cml:radios label="bacon" only-if="omg:[crispy]||w_t_f:[rtfm]++f_t_w:[rtfm]">
136
+ <cml:radio label="tasty" />
137
+ <cml:radio label="too meaty" />
138
+ </cml:radios>
139
+ HTML
140
+
141
+ graph = @p.logic_tree.graph
142
+ graph.should == {
143
+ "omg"=>{:outbound_count=>3, :outbound_names=>["bacon", "f_t_w", "w_t_f"]},
144
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
145
+ :outbound_count=>1, :outbound_names=>["bacon"]},
146
+ "f_t_w"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
147
+ :outbound_count=>1, :outbound_names=>["bacon"]},
148
+ "bacon"=>{:inbound_count=>3, :inbound_names=>["omg", "w_t_f", "f_t_w"],
149
+ :inbound_matches=>{
150
+ "||" => [
151
+ {"omg"=>[{:match_key => "crispy", :is_not=>false}]},
152
+ {"&&"=>[
153
+ {"w_t_f"=>[{:match_key => "rtfm", :is_not=>false}]},
154
+ {"f_t_w"=>[{:match_key => 'rtfm', :is_not=>false}]}
155
+ ]}
156
+ ]
157
+ }
158
+ }
159
+ }
160
+ @p.logic_tree.nodes.collect {|k,v|
161
+ v.tag.errors && v.tag.errors.each {|err|
162
+ err.should =~ /^Tag logic is not accurate when only-if contains a group/ }}
163
+ end
164
+
165
+ it "lists complex nested only-if 'or' selectors" do
166
+ @p = Parser.new(<<-HTML)
167
+ <cml:text label="OMG" />
168
+ <cml:checkboxes label="w t f" only-if="omg">
169
+ <cml:checkbox label="lol" />
170
+ <cml:checkbox label="rtfm" />
171
+ </cml:checkboxes>
172
+ <cml:checkboxes label="f t w" only-if="omg">
173
+ <cml:checkbox label="lmao" />
174
+ <cml:checkbox label="rtfm" />
175
+ </cml:checkboxes>
176
+ <cml:radios label="bacon" only-if="omg:[crispy]||(w_t_f:[rtfm]++f_t_w:[rtfm]||f_t_w:[lmao])">
177
+ <cml:radio label="tasty" />
178
+ <cml:radio label="too meaty" />
179
+ </cml:radios>
180
+ HTML
181
+
182
+ graph = @p.logic_tree.graph
183
+ graph.should == {
184
+ "omg"=>{:outbound_count=>3, :outbound_names=>["bacon", "f_t_w", "w_t_f"]},
185
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
186
+ :outbound_count=>1, :outbound_names=>["bacon"]},
187
+ "f_t_w"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
188
+ :outbound_count=>1, :outbound_names=>["bacon"]},
189
+ "bacon"=>{:inbound_count=>3, :inbound_names=>["omg", "w_t_f", "f_t_w"],
190
+ :inbound_matches=>{
191
+ "||" => [
192
+ {"omg"=>[{:match_key => "crispy", :is_not=>false}]},
193
+ {"||"=> [
194
+ {"&&"=>[
195
+ {"w_t_f"=>[{:match_key => "rtfm", :is_not=>false}]},
196
+ {"f_t_w"=>[{:match_key => 'rtfm', :is_not=>false}]}
197
+ ]},
198
+ {"f_t_w"=>[{:match_key => "lmao", :is_not=>false}]}
199
+ ]}
200
+ ]
201
+ }
202
+ }
203
+ }
204
+ @p.logic_tree.nodes.collect {|k,v|
205
+ v.tag.errors && v.tag.errors.each {|err|
206
+ err.should =~ /^Tag logic is not accurate when only-if contains a group/ }}
207
+ end
208
+
209
+ it "lists complex nested only-if 'and' selectors" do
210
+ @p = Parser.new(<<-HTML)
211
+ <cml:text label="OMG" />
212
+ <cml:checkboxes label="w t f" only-if="omg">
213
+ <cml:checkbox label="lol" />
214
+ <cml:checkbox label="rtfm" />
215
+ </cml:checkboxes>
216
+ <cml:checkboxes label="f t w" only-if="omg">
217
+ <cml:checkbox label="lmao" />
218
+ <cml:checkbox label="rtfm" />
219
+ </cml:checkboxes>
220
+ <cml:radios label="bacon" only-if="omg:[crispy]++(w_t_f:[rtfm]++f_t_w:[rtfm]||f_t_w:[lmao])">
221
+ <cml:radio label="tasty" />
222
+ <cml:radio label="too meaty" />
223
+ </cml:radios>
224
+ HTML
225
+
226
+ graph = @p.logic_tree.graph
227
+ graph.should == {
228
+ "omg"=>{:outbound_count=>3, :outbound_names=>["bacon", "f_t_w", "w_t_f"]},
229
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
230
+ :outbound_count=>1, :outbound_names=>["bacon"]},
231
+ "f_t_w"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key => nil, :is_not=>true}]}]},
232
+ :outbound_count=>1, :outbound_names=>["bacon"]},
233
+ "bacon"=>{:inbound_count=>3, :inbound_names=>["omg", "w_t_f", "f_t_w"],
234
+ :inbound_matches=>{
235
+ "&&" => [
236
+ {"omg"=>[{:match_key => "crispy", :is_not=>false}]},
237
+ {"||"=> [
238
+ {"&&"=>[
239
+ {"w_t_f"=>[{:match_key => "rtfm", :is_not=>false}]},
240
+ {"f_t_w"=>[{:match_key => 'rtfm', :is_not=>false}]}
241
+ ]},
242
+ {"f_t_w"=>[{:match_key => "lmao", :is_not=>false}]}
243
+ ]}
244
+ ]
245
+ }
246
+ }
247
+ }
248
+ @p.logic_tree.nodes.collect {|k,v|
249
+ v.tag.errors && v.tag.errors.each {|err|
250
+ err.should =~ /^Tag logic is not accurate when only-if contains a group/ }}
251
+ end
252
+
253
+ it "is aware of only-if group" do
254
+ @p = Parser.new(<<-HTML)
255
+ <cml:text label="OMG" />
256
+ <cml:group only-if="omg">
257
+ <cml:checkboxes label="w t f">
258
+ <cml:checkbox label="lol" />
259
+ <cml:checkbox label="rtfm" />
260
+ </cml:checkboxes>
261
+ <cml:radios label="bacon">
262
+ <cml:radio label="tasty" />
263
+ <cml:radio label="too meaty" />
264
+ </cml:radios>
265
+ </cml:group>
266
+ HTML
267
+
268
+ graph = @p.logic_tree.graph
269
+ graph.should == {
270
+ "omg"=>{:outbound_count=>2, :outbound_names=>["bacon", "w_t_f"]},
271
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key=>nil, :is_not=>true}]}]}},
272
+ "bacon"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key=>nil, :is_not=>true}]}]}}
273
+ }
274
+ end
275
+
276
+ it "is aware of nested only-if group" do
277
+ @p = Parser.new(<<-HTML)
278
+ <cml:text label="OMG" />
279
+ <cml:group only-if="omg">
280
+ <cml:checkboxes label="w t f">
281
+ <cml:checkbox label="lol" />
282
+ <cml:checkbox label="rtfm" />
283
+ </cml:checkboxes>
284
+ <cml:group only-if="!w_t_f:[rtfm]">
285
+ <cml:radios label="bacon">
286
+ <cml:radio label="tasty" />
287
+ <cml:radio label="too meaty" />
288
+ </cml:radios>
289
+ </cml:group>
290
+ </cml:group>
291
+ HTML
292
+
293
+ graph = @p.logic_tree.graph
294
+ graph.should == {
295
+ "omg"=>{:outbound_count=>2, :outbound_names=>["bacon", "w_t_f"]},
296
+ "w_t_f"=>{:inbound_count=>1, :inbound_names=>["omg"], :inbound_matches=>{"||" => [{"omg"=>[{:match_key=>nil, :is_not=>true}]}]},
297
+ :outbound_count=>1, :outbound_names=>["bacon"]},
298
+ "bacon"=>{:inbound_count=>2, :inbound_names=>["omg", "w_t_f"], :inbound_matches=>{
299
+ "||" => [
300
+ {"omg"=>[{:match_key=>nil, :is_not=>true}]},
301
+ {"w_t_f"=>[{:match_key=>"rtfm", :is_not=>true}]}
302
+ ]
303
+ }}
304
+ }
305
+ end
306
+
307
+ it "handles inter-dependent logic" do
308
+ @p = Parser.new(<<-HTML)
309
+ <cml:group only-if="details_unavailable:unchecked">
310
+
311
+ <cml:radios name="local" label="Is this a 'Local' or 'National' deal?" validates="required" gold="true">
312
+ <cml:radio value="local" label="LOCAL: The business or activity is only local" duplicate="true"></cml:radio>
313
+ <cml:radio value="local" label="LOCAL: The product is specific to a city or region (e.g. a local magazine or a product with shipping restrictions)" duplicate="true"></cml:radio>
314
+ <cml:radio value="local" label="LOCAL: This travel or hotel deal appeals to people in a particular city only (e.g. a ONE-NIGHT stay in a hotel room or bed and breakfast at a non-tourist destination)" duplicate="true"></cml:radio>
315
+ <cml:radio value="national" label="NATIONAL: This deal appeals to anyone in the country, and has NO regional shipping restrictions" duplicate="true"></cml:radio>
316
+ <cml:radio value="national" label="NATIONAL: This travel or hotel deal appeals to anyone in the country (e.g. A MULTI-NIGHT stay in a tourist destination such as Las Vegas, Miami, or another country.)" duplicate="true"></cml:radio>
317
+ </cml:radios>
318
+
319
+ <cml:group only-if="local:[0]||local:[1]||local:[2]">
320
+
321
+ <cml:radios name="location" label="Does the consumer have to leave their home?" validates="required" gold="true">
322
+ <cml:radio value="location_important" label="YES: There is one (or few) specific locations."></cml:radio>
323
+ <cml:radio value="location_unimportant" label="NO: This deal is redeemed from home or delivered to the home" duplicate="true"></cml:radio>
324
+ </cml:radios>
325
+
326
+ <cml:taxonomy label="Please select the best LOCAL deal category or categories (you may select more than one if multiple are great fits):" validates="required" multi-select="true" instructions="Any category that you have selected will appear here. Click the blue 'x' to unselect a category." strict="true" aggregation="cagg_0.4" name="local_category" log-search="true"></cml:taxonomy>
327
+
328
+ </cml:group>
329
+
330
+ <cml:group only-if="local:[3]||local:[4]">
331
+
332
+ <cml:taxonomy label="Please select the best NATIONAL deal category or categories (you may select more than one if multiple are great fits):" multi-select="true" name="national_category" validates="required" strict="true" gold="true" aggregation="cagg_0.4" log-search="true"></cml:taxonomy>
333
+ </cml:group>
334
+ </cml:group>
335
+ <hr />
336
+
337
+ <cml:checkbox label="Click here if the details of the deal are unavailable." default="false" name="details_unavailable" instructions="Do NOT check this box simply because a deal is expired." gold="true"></cml:checkbox>
338
+
339
+ HTML
340
+
341
+ graph = @p.logic_tree.graph
342
+ graph.should == {
343
+ "local"=>{
344
+ :inbound_count=>1, :inbound_names=>["details_unavailable"], :inbound_matches=>{
345
+ "||" => [
346
+ {"details_unavailable"=>[{:match_key=>"true", :is_not=>true}]}
347
+ ]},
348
+ :outbound_count=>3, :outbound_names => %w[local_category location national_category]},
349
+ "location"=>{
350
+ :inbound_count=>2, :inbound_names=>["details_unavailable", "local"], :inbound_matches=>{
351
+ "||" => [
352
+ {"details_unavailable"=>[{:match_key=>"true", :is_not=>true}]},
353
+ {"local"=>[{:match_key=>"local", :is_not=>false}]}
354
+ ]
355
+ }
356
+ },
357
+ "details_unavailable"=>{
358
+ :outbound_count=>4, :outbound_names=> %w[local local_category location national_category]},
359
+ "national_category"=>{:inbound_count=>2, :inbound_names=>["details_unavailable", "local"], :inbound_matches=>{
360
+ "||" => [
361
+ {"details_unavailable"=>[{:match_key=>"true", :is_not=>true}]},
362
+ {"local"=>[{:match_key=>"national", :is_not=>false}]}
363
+ ]
364
+ }},
365
+ "local_category"=>{:inbound_count=>2, :inbound_names=>["details_unavailable", "local"], :inbound_matches=>{
366
+ "||" => [
367
+ {"details_unavailable"=>[{:match_key=>"true", :is_not=>true}]},
368
+ {"local"=>[{:match_key=>"local", :is_not=>false}]}
369
+ ]
370
+ }}
371
+ }
372
+ end
373
+
374
+ it "lists a simple only-if selector with indexed match" do
375
+ @p = Parser.new(<<-HTML)
376
+ <cml:checkboxes label="w t f">
377
+ <cml:checkbox label="lol" />
378
+ <cml:checkbox label="rtfm" />
379
+ </cml:checkboxes>
380
+ <cml:text label="OMG" only-if="w_t_f:[1]" />
381
+ HTML
382
+
383
+ graph = @p.logic_tree.graph
384
+ graph.should == {
385
+ "w_t_f"=>{:outbound_count=>1, :outbound_names=>["omg"]},
386
+ "omg"=>{:inbound_count=>1, :inbound_names=>["w_t_f"], :inbound_matches=>{
387
+ "||" => [
388
+ {"w_t_f"=>[{:match_key=>"rtfm", :is_not=>false}]}
389
+ ]
390
+ }}
391
+ }
392
+ end
393
+
394
+ it "gracefully skips logic containing Liquid variables" do
395
+ @p = Parser.new(<<-HTML)
396
+ <div class="user_input">
397
+ {% for engine in unique_engines %}
398
+ <div id="{{ _unit_id }}_input_engine_{{ forloop.index }}">
399
+ {% case forloop.index %}
400
+ {% when 1 %}
401
+ <cml:ratings gold="true" label="User Satisfaction of Engine 1" name="rating_engine_1" validates="required">
402
+ <cml:rating label="Can't Tell / Invalid Search" value="N/A"></cml:rating>
403
+ <cml:rating label="Bad" value="1"></cml:rating>
404
+ <cml:rating label="OK" value="2"></cml:rating>
405
+ <cml:rating label="Perfect" value="3"></cml:rating>
406
+ </cml:ratings>
407
+ <cml:checkboxes label="Please specify reason(s) for your rating" name="reasons_engine_1" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]" strict="true" gold="true">
408
+ <cml:checkbox label="I expected result(s) but none were returned"></cml:checkbox>
409
+ <cml:checkbox label="There are duplicate listings in the results"></cml:checkbox>
410
+ <cml:checkbox label="The ranking of the listings is incorrect"></cml:checkbox>
411
+ </cml:checkboxes>
412
+ <cml:textarea label="Comments" name="comments_engine_1" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]"></cml:textarea>
413
+ {% when 2 %}
414
+ <cml:ratings gold="true" label="User Satisfaction of Engine 2" name="rating_engine_2" validates="required">
415
+ <cml:rating label="Can't Tell / Invalid Search" value="N/A"></cml:rating>
416
+ <cml:rating label="Bad" value="1"></cml:rating>
417
+ <cml:rating label="OK" value="2"></cml:rating>
418
+ <cml:rating label="Perfect" value="3"></cml:rating>
419
+ <cml:checkbox label="Other (specify in comments below)" value="Other"></cml:checkbox>
420
+ </cml:ratings>
421
+ <cml:checkboxes label="Please specify reason(s) for your rating" name="reasons_engine_2" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]" strict="true" gold="true">
422
+ <cml:checkbox label="I expected result(s) but none were returned"></cml:checkbox>
423
+ <cml:checkbox label="There are duplicate listings in the results"></cml:checkbox>
424
+ <cml:checkbox label="The ranking of the listings is incorrect"></cml:checkbox>
425
+ </cml:checkboxes>
426
+ <cml:textarea label="Comments" name="comments_engine_2" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]"></cml:textarea>
427
+ {% when 3 %}
428
+ <cml:ratings gold="true" label="User Satisfaction of Engine 3" name="rating_engine_3" validates="required">
429
+ <cml:rating label="Can't Tell / Invalid Search" value="N/A"></cml:rating>
430
+ <cml:rating label="Bad" value="1"></cml:rating>
431
+ <cml:rating label="OK" value="2"></cml:rating>
432
+ <cml:rating label="Perfect" value="3"></cml:rating>
433
+ </cml:ratings>
434
+ <cml:checkboxes label="Please specify reason(s) for your rating" name="reasons_engine_3" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]" strict="true" gold="true">
435
+ <cml:checkbox label="I expected result(s) but none were returned"></cml:checkbox>
436
+ <cml:checkbox label="There are duplicate listings in the results"></cml:checkbox>
437
+ <cml:checkbox label="The ranking of the listings is incorrect"></cml:checkbox>
438
+ </cml:checkboxes>
439
+ <cml:textarea label="Comments" name="comments_engine_3" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]"></cml:textarea>
440
+ {% when 4 %}
441
+ <cml:ratings gold="true" label="User Satisfaction of Engine 4" name="rating_engine_4" validates="required">
442
+ <cml:rating label="Can't Tell / Invalid Search" value="N/A"></cml:rating>
443
+ <cml:rating label="Bad" value="1"></cml:rating>
444
+ <cml:rating label="OK" value="2"></cml:rating>
445
+ <cml:rating label="Perfect" value="3"></cml:rating>
446
+ </cml:ratings>
447
+ <cml:checkboxes label="Please specify reason(s) for your rating" name="reasons_engine_4" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]" strict="true" gold="true">
448
+ <cml:checkbox label="I expected result(s) but none were returned"></cml:checkbox>
449
+ <cml:checkbox label="There are duplicate listings in the results"></cml:checkbox>
450
+ <cml:checkbox label="The ranking of the listings is incorrect"></cml:checkbox>
451
+ </cml:checkboxes>
452
+ <cml:textarea label="Comments" name="comments_engine_4" only-if="rating_engine_{{ forloop.index }}:[1]||rating_engine_{{ forloop.index }}:[2]"></cml:textarea>
453
+ {% endcase %}
454
+ </div>
455
+ {% endfor %}
456
+ </div>
457
+ HTML
458
+
459
+ @p.logic_tree.graph.each {|k,v| v.should == {} }
460
+ @p.logic_tree.nodes.collect {|k,v|
461
+ v.tag.errors && v.tag.errors.each {|err|
462
+ err.should =~ /^The logic tree cannot be constructed when only-if contains a Liquid tag/ }}
463
+ end
464
+
465
+ end