cml 1.4.2 → 1.5.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.
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