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.
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +33 -0
- data/README.rdoc +13 -3
- data/Rakefile +9 -49
- data/cml.gemspec +23 -125
- data/lib/cml/converters/jsawesome.rb +3 -3
- data/lib/cml/gold.rb +12 -7
- data/lib/cml/liquid_filters.rb +4 -0
- data/lib/cml/logic.rb +424 -0
- data/lib/cml/logic_tree/graph.rb +107 -0
- data/lib/cml/logic_tree/solver.rb +43 -0
- data/lib/cml/parser.rb +47 -7
- data/lib/cml/tag.rb +42 -21
- data/lib/cml/tags/checkbox.rb +14 -4
- data/lib/cml/tags/checkboxes.rb +4 -0
- data/lib/cml/tags/group.rb +4 -0
- data/lib/cml/tags/hours.rb +263 -0
- data/lib/cml/tags/iterate.rb +4 -0
- data/lib/cml/tags/meta.rb +4 -0
- data/lib/cml/tags/option.rb +4 -0
- data/lib/cml/tags/radio.rb +13 -4
- data/lib/cml/tags/radios.rb +4 -0
- data/lib/cml/tags/ratings.rb +9 -1
- data/lib/cml/tags/search.rb +8 -2
- data/lib/cml/tags/select.rb +6 -2
- data/lib/cml/tags/taxonomy.rb +148 -0
- data/lib/cml/tags/thumb.rb +4 -0
- data/lib/cml/tags/unknown.rb +4 -0
- data/lib/cml/version.rb +3 -0
- data/lib/cml.rb +3 -0
- data/spec/converters/jsawesome_spec.rb +0 -9
- data/spec/fixtures/logic_broken.cml +5 -0
- data/spec/fixtures/logic_circular.cml +5 -0
- data/spec/fixtures/logic_grouped.cml +7 -0
- data/spec/fixtures/logic_nested.cml +7 -0
- data/spec/fixtures/logic_none.cml +7 -0
- data/spec/fixtures/logic_not_nested.cml +7 -0
- data/spec/liquid_filter_spec.rb +19 -0
- data/spec/logic_depends_on_spec.rb +242 -0
- data/spec/logic_spec.rb +207 -0
- data/spec/logic_tree_graph_spec.rb +465 -0
- data/spec/logic_tree_solver_spec.rb +58 -0
- data/spec/meta_spec.rb +12 -2
- data/spec/show_data_spec.rb +3 -2
- data/spec/spec_helper.rb +22 -6
- data/spec/tags/checkboxes_spec.rb +2 -2
- data/spec/tags/group_spec.rb +5 -5
- data/spec/tags/hours_spec.rb +404 -0
- data/spec/tags/radios_spec.rb +2 -2
- data/spec/tags/ratings_spec.rb +1 -1
- data/spec/tags/select_spec.rb +45 -0
- data/spec/tags/tag_spec.rb +25 -0
- data/spec/tags/taxonomy_spec.rb +112 -0
- data/spec/validation_spec.rb +52 -0
- metadata +112 -17
- data/VERSION +0 -1
data/spec/logic_spec.rb
ADDED
@@ -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
|