saxxy 0.1.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +13 -0
  5. data/LICENSE +22 -0
  6. data/README.md +117 -0
  7. data/Rakefile +12 -0
  8. data/lib/saxxy.rb +2 -0
  9. data/lib/saxxy/activatable.rb +160 -0
  10. data/lib/saxxy/callbacks/libxml.rb +26 -0
  11. data/lib/saxxy/callbacks/nokogiri.rb +30 -0
  12. data/lib/saxxy/callbacks/ox.rb +66 -0
  13. data/lib/saxxy/callbacks/sax.rb +86 -0
  14. data/lib/saxxy/context.rb +88 -0
  15. data/lib/saxxy/context_tree.rb +85 -0
  16. data/lib/saxxy/event.rb +83 -0
  17. data/lib/saxxy/event_registry.rb +122 -0
  18. data/lib/saxxy/node_action.rb +59 -0
  19. data/lib/saxxy/node_rule.rb +90 -0
  20. data/lib/saxxy/parsers/base.rb +28 -0
  21. data/lib/saxxy/parsers/libxml.rb +52 -0
  22. data/lib/saxxy/parsers/nokogiri.rb +28 -0
  23. data/lib/saxxy/parsers/ox.rb +30 -0
  24. data/lib/saxxy/service.rb +47 -0
  25. data/lib/saxxy/utils/agent.rb +66 -0
  26. data/lib/saxxy/utils/callback_array.rb +27 -0
  27. data/lib/saxxy/utils/helpers.rb +13 -0
  28. data/lib/saxxy/version.rb +3 -0
  29. data/saxxy.gemspec +21 -0
  30. data/spec/saxxy/activatable_spec.rb +344 -0
  31. data/spec/saxxy/callbacks/sax_spec.rb +456 -0
  32. data/spec/saxxy/context_spec.rb +51 -0
  33. data/spec/saxxy/context_tree_spec.rb +68 -0
  34. data/spec/saxxy/event_registry_spec.rb +137 -0
  35. data/spec/saxxy/event_spec.rb +49 -0
  36. data/spec/saxxy/node_action_spec.rb +46 -0
  37. data/spec/saxxy/node_rule_spec.rb +99 -0
  38. data/spec/saxxy/parsers/libxml_spec.rb +104 -0
  39. data/spec/saxxy/parsers/nokogiri_spec.rb +200 -0
  40. data/spec/saxxy/parsers/ox_spec.rb +175 -0
  41. data/spec/saxxy/utils/agent_spec.rb +63 -0
  42. data/spec/spec_helper.rb +28 -0
  43. data/spec/support/agent_macros.rb +24 -0
  44. metadata +155 -0
@@ -0,0 +1,3 @@
1
+ module Saxxy
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/saxxy/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["rubymaniac"]
6
+ gem.description = %q{A Ruby DSL for building SAX parsers.}
7
+ gem.summary = %q{Constructing SAX parsers never been easier.}
8
+ gem.homepage = "https://github.com/rubymaniac/saxxy"
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "saxxy"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = Saxxy::VERSION
16
+
17
+ gem.add_development_dependency "rake"
18
+ gem.add_development_dependency "rspec", "~> 2.11"
19
+ gem.add_development_dependency "fakeweb"
20
+ gem.add_development_dependency "pry"
21
+ end
@@ -0,0 +1,344 @@
1
+ require 'spec_helper'
2
+ require 'saxxy/node_rule'
3
+ require 'saxxy/activatable'
4
+
5
+
6
+ describe Saxxy::Activatable do
7
+
8
+ let(:klass) do
9
+ Class.new do
10
+ include Saxxy::Activatable
11
+ attr_reader :deactivation_level, :deactivation_callback, :activation_callback, :mode
12
+ end
13
+ end
14
+ let(:subject) { klass.new }
15
+ let(:rule) { Saxxy::NodeRule.new("div", class: /foo$/) }
16
+
17
+
18
+ describe "#included" do
19
+ it "should set an attribute reader for the activation_rule" do
20
+ a_class = Class.new
21
+ a_class.should_receive(:attr_reader).with(:activation_rule)
22
+ Saxxy::Activatable.included(a_class)
23
+ end
24
+ end
25
+
26
+
27
+ describe "#initialize_activatable" do
28
+ it "should set the activation rule" do
29
+ expect { subject.initialize_activatable(rule) }.to change { subject.activation_rule }.from(nil).to(rule)
30
+ end
31
+
32
+ it "should set the deactivation level" do
33
+ expect { subject.initialize_activatable(rule) }.to change {
34
+ subject.deactivation_level
35
+ }.from(nil).to(Saxxy::Activatable::DLEVEL_MIN)
36
+ end
37
+
38
+ it "should set the mode to inactive" do
39
+ expect { subject.initialize_activatable(rule) }.to change { subject.mode }.from(nil).to(:inactive)
40
+ end
41
+ end
42
+
43
+
44
+ describe "#on_deactivation" do
45
+ let(:block) { ->(e) { e } }
46
+
47
+ it "should set the deactivation callback" do
48
+ expect { subject.on_deactivation(&block) }.to change { subject.deactivation_callback }.to(block)
49
+ end
50
+
51
+ it "should return self" do
52
+ subject.on_deactivation(&block).should == subject
53
+ end
54
+ end
55
+
56
+
57
+ describe "#on_activation" do
58
+ let(:block) { ->(e) { e } }
59
+
60
+ it "should set the activation callback" do
61
+ expect { subject.on_activation(&block) }.to change { subject.activation_callback }.to(block)
62
+ end
63
+
64
+ it "should return self" do
65
+ subject.on_activation(&block).should == subject
66
+ end
67
+ end
68
+
69
+
70
+ describe "#can_be_activated_on" do
71
+ context "with activation rule" do
72
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) } }
73
+
74
+ it "should return true if rule matches the node and is closed" do
75
+ subject.stub(closed?: true)
76
+ subject.send(:can_be_activated_on, "div", { class: "baz foo" }).should be_true
77
+ end
78
+
79
+ it "should return false if rule doen't match the node and is closed" do
80
+ subject.stub(closed?: true)
81
+ subject.send(:can_be_activated_on, "div", { class: "baz foo4" }).should be_false
82
+ end
83
+
84
+ it "should return false if rule matches and is not closed" do
85
+ subject.stub(closed?: false)
86
+ subject.send(:can_be_activated_on, "div", { class: "baz foo" }).should be_false
87
+ end
88
+ end
89
+
90
+ context "without activation rule" do
91
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(nil) } }
92
+
93
+ it "should return true on any node if closed" do
94
+ subject.stub(closed?: true)
95
+ subject.send(:can_be_activated_on, "d**", { class: "cREfrt34%^f" }).should be_true
96
+ end
97
+
98
+ it "should return false on any node if not closed" do
99
+ subject.stub(closed?: false)
100
+ subject.send(:can_be_activated_on, "d**", { class: "cREfrt34%^f" }).should be_false
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+ describe "#switch_to" do
107
+ it "should change the mode to the specified argument" do
108
+ expect { subject.send(:switch_to, :active) }.to change { subject.mode }.from(nil).to(:active)
109
+ end
110
+ end
111
+
112
+
113
+ describe "#is" do
114
+ it "should return true if is in the same mode as the argument" do
115
+ subject.send(:switch_to, :active)
116
+ subject.send(:is, :active).should be_true
117
+ end
118
+
119
+ it "should return false if is not in the same mode as the argument" do
120
+ subject.send(:switch_to, :inactive)
121
+ subject.send(:is, :active).should be_false
122
+ end
123
+ end
124
+
125
+
126
+ describe "#activate_on" do
127
+ context "inactive mode" do
128
+ let(:callback) { ->(e) { e } }
129
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) }.on_activation(&callback) }
130
+
131
+ it "should not run the callback if rule does not match" do
132
+ args1 = ["div", { class: "foo4" }]
133
+ args2 = ["span", { class: "foo" }]
134
+ subject.activation_rule.matches(*args1).should be_false
135
+ subject.activation_rule.matches(*args2).should be_false
136
+ callback.should_not_receive(:call)
137
+ subject.activate_on(*args1)
138
+ subject.activate_on(*args2)
139
+ end
140
+
141
+ it "should run the callback if it has rule and matches the node" do
142
+ args = ["div", { class: "foo" }]
143
+ subject.activation_rule.matches(*args).should be_true
144
+ callback.should_receive(:call)
145
+ subject.activate_on(*args)
146
+ end
147
+
148
+ it "should not increment the level if it cannot be activated" do
149
+ expect { subject.activate_on("div", class: "foo4") }.to_not change { subject.deactivation_level }
150
+ expect { subject.activate_on("span", class: "foo") }.to_not change { subject.deactivation_level }
151
+ end
152
+
153
+ it "should increment the level if it can be activated" do
154
+ expect { subject.activate_on("div", class: "foo") }.to change { subject.deactivation_level }.by(1)
155
+ end
156
+
157
+ it "should change the mode if it can be activated" do
158
+ expect { subject.activate_on("div", class: "foo") }.to change {
159
+ subject.mode
160
+ }.from(:inactive).to(:active)
161
+ end
162
+ end
163
+
164
+ context "active mode" do
165
+ let(:subject) do
166
+ klass.new.tap do |i|
167
+ i.initialize_activatable(rule)
168
+ i.activate_on("div", class: "foo")
169
+ end
170
+ end
171
+
172
+ before { subject.send(:is, :active).should be_true }
173
+
174
+ it "should not run the callback if it can be activated" do
175
+ subject.on_activation(&->(){})
176
+ subject.activation_callback.should_not_receive(:call)
177
+ subject.activate_on("div", class: "foo")
178
+ end
179
+
180
+ it "should not increment the level if rule doesn't match element name" do
181
+ expect { subject.activate_on("div3", class: "foo") }.to_not change { subject.deactivation_level }
182
+ end
183
+
184
+ it "should increment the level if rule matches element name" do
185
+ expect { subject.activate_on("div", class: "foo43") }.to change { subject.deactivation_level }.by(1)
186
+ end
187
+
188
+ it "should not change the mode if rule matches element name" do
189
+ expect { subject.activate_on("div", class: "foo") }.to_not change { subject.mode }
190
+ end
191
+ end
192
+ end
193
+
194
+
195
+ describe "#deactivate_on" do
196
+ let(:callback) { ->(e) { e } }
197
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) }.on_deactivation(&callback) }
198
+
199
+ context "inactive mode" do
200
+ it "should not call decrement_level or deactivate! if rule does not match element name" do
201
+ callback.should_not_receive(:call)
202
+ subject.should_not_receive(:decrement_level)
203
+ subject.should_not_receive(:deactivate!)
204
+ subject.deactivate_on("divs")
205
+ end
206
+
207
+ it "should not call decrement_level or deactivate! if rule matches element name" do
208
+ callback.should_not_receive(:call)
209
+ subject.should_not_receive(:decrement_level)
210
+ subject.should_not_receive(:deactivate!)
211
+ subject.deactivate_on("div")
212
+ end
213
+
214
+ it "should not change the mode when is deactivated in a matching element name" do
215
+ expect { subject.deactivate_on("div") }.to_not change { subject.mode }
216
+ end
217
+
218
+ it "should not change the mode when is deactivated in non matching element name" do
219
+ expect { subject.deactivate_on("divd") }.to_not change { subject.mode }
220
+ end
221
+ end
222
+
223
+ context "active mode" do
224
+ before do
225
+ subject.activate_on("div", class: "foo")
226
+ subject.send(:is, :active).should be_true
227
+ end
228
+
229
+ it "should not run the callback if it isnt deactivated as many times as gets activated i.e. is not closed" do
230
+ callback.should_not_receive(:call).with(subject)
231
+ 4.times { |i| subject.activate_on("div", class: "foo#{i}") }
232
+ 4.times { subject.deactivate_on("div") }
233
+ end
234
+
235
+ it "should run the deactivation callback if it is deactivated as many times as gets activated i.e. is closed" do
236
+ callback.should_receive(:call).with(subject)
237
+ 3.times { |i| subject.activate_on("div", class: "foo#{i}") }
238
+ 4.times { subject.deactivate_on("div") }
239
+ end
240
+
241
+ it "should not change mode to inactive if it isnt deactivated as many times as gets activated i.e. is not closed" do
242
+ expect do
243
+ 4.times { |i| subject.activate_on("div", class: "foo#{i}") }
244
+ 4.times { subject.deactivate_on("div") }
245
+ end.to_not change { subject.mode }
246
+ end
247
+
248
+ it "should change mode to inactive if it is deactivated as many times as gets activated i.e. is closed" do
249
+ expect do
250
+ 3.times { |i| subject.activate_on("div", class: "foo#{i}") }
251
+ 4.times { subject.deactivate_on("div") }
252
+ end.to change { subject.mode }.from(:active).to(:inactive)
253
+ end
254
+
255
+ it "should run the deactivation callback if rule matches element name" do
256
+ callback.should_receive(:call).with(subject)
257
+ subject.deactivate_on("div")
258
+ end
259
+
260
+ it "should not run the deactivation callback if rule doesn't match element name" do
261
+ callback.should_not_receive(:call).with(subject)
262
+ subject.deactivate_on("divd")
263
+ end
264
+
265
+ it "should decrement the level when deactivated on a matching element name" do
266
+ expect { subject.deactivate_on("div") }.to change { subject.deactivation_level }.by(-1)
267
+ end
268
+
269
+ it "should not decrement the level when deactivated on a non matching element name" do
270
+ expect { subject.deactivate_on("divd") }.to_not change { subject.deactivation_level }
271
+ end
272
+ end
273
+ end
274
+
275
+
276
+ describe "#run_activation_callback" do
277
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) } }
278
+
279
+ it "should call the activation_callback if it is present" do
280
+ subject.on_activation(&->(){})
281
+ subject.instance_variable_get("@activation_callback").should_receive(:call).with(subject)
282
+ subject.send(:run_activation_callback)
283
+ end
284
+
285
+ it "should not call the activation_callback if it is not present" do
286
+ allow_message_expectations_on_nil
287
+ subject.instance_variable_get("@activation_callback").should_not_receive(:call).with(subject)
288
+ subject.send(:run_activation_callback)
289
+ end
290
+ end
291
+
292
+
293
+ describe "#run_deactivation_callback" do
294
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) } }
295
+
296
+ it "should call the deactivation_callback if it is present" do
297
+ subject.on_deactivation(&->(){})
298
+ subject.instance_variable_get("@deactivation_callback").should_receive(:call).with(subject)
299
+ subject.send(:run_deactivation_callback)
300
+ end
301
+
302
+ it "should not call the deactivation callback if it is not present" do
303
+ allow_message_expectations_on_nil
304
+ subject.instance_variable_get("@deactivation_callback").should_not_receive(:call).with(subject)
305
+ subject.send(:run_deactivation_callback)
306
+ end
307
+ end
308
+
309
+
310
+ describe "#increment_level" do
311
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) } }
312
+
313
+ it "should increment the deactivation_level" do
314
+ expect { subject.send(:increment_level) }.to change { subject.deactivation_level }.by(1)
315
+ end
316
+ end
317
+
318
+
319
+ describe "#decrement_level" do
320
+ let(:subject) { klass.new.tap { |i| i.initialize_activatable(rule) } }
321
+
322
+ it "should decrement the deactivation_level" do
323
+ expect { subject.send(:decrement_level) }.to change { subject.deactivation_level }.by(-1)
324
+ end
325
+ end
326
+
327
+
328
+ describe "#rule_matches_element_name" do
329
+ it "should return true if no activation rule" do
330
+ subject.send(:rule_matches_element_name, "foo").should be_true
331
+ end
332
+
333
+ it "should return false if it has activation_rule and does not match the element name" do
334
+ subject.initialize_activatable(rule)
335
+ subject.send(:rule_matches_element_name, "foo").should be_false
336
+ end
337
+
338
+ it "should return true if it has activation_rule and does match the element name" do
339
+ subject.initialize_activatable(rule)
340
+ subject.send(:rule_matches_element_name, "div").should be_true
341
+ end
342
+ end
343
+
344
+ end
@@ -0,0 +1,456 @@
1
+ require "saxxy/utils/callback_array"
2
+ require "saxxy/context"
3
+ require "saxxy/node_rule"
4
+ require "saxxy/event_registry"
5
+ require "saxxy/context_tree"
6
+ require "saxxy/callbacks/sax"
7
+
8
+
9
+ describe Saxxy::Callbacks::SAX do
10
+
11
+ # A class that includes the SAXCallbacks functionality
12
+ let(:klass) do
13
+ Class.new do
14
+ include Saxxy::Callbacks::SAX
15
+ attr_reader :active_pool, :inactive_pool, :event_registry
16
+ end
17
+ end
18
+
19
+ # Helper methods
20
+ def get_level(obj = subject)
21
+ obj.instance_variable_get("@deactivation_level")
22
+ end
23
+
24
+ def pool(type = :active, obj = subject)
25
+ obj.instance_variable_get("@#{type}_pool")
26
+ end
27
+
28
+ def registry
29
+ subject.event_registry.instance_variable_get("@actions")
30
+ end
31
+
32
+ def events
33
+ registry.values
34
+ end
35
+
36
+ def actions
37
+ registry.keys
38
+ end
39
+
40
+
41
+ describe "#initialize" do
42
+ let(:context) do
43
+ Saxxy::ContextTree.new do
44
+ under("div", class: "foo") do
45
+ under("span", title: /bar/)
46
+ end
47
+
48
+ under("table")
49
+ end.root
50
+ end
51
+ let(:subject) { klass.new(context) }
52
+
53
+ it "should set the active pool to a callback array" do
54
+ subject.active_pool.should be_a(Saxxy::CallbackArray)
55
+ subject.active_pool.instance_variable_get("@add_callback").should_not be_nil
56
+ end
57
+
58
+ it "should set the inactive pool to a callback array" do
59
+ subject.inactive_pool.should be_a(Saxxy::CallbackArray)
60
+ subject.inactive_pool.instance_variable_get("@add_callback").should_not be_nil
61
+ end
62
+
63
+ it "should set the event registry" do
64
+ subject.instance_variable_get("@event_registry").should be_a(Saxxy::EventRegistry)
65
+ end
66
+
67
+ it "should have the context in the active_pool" do
68
+ subject.active_pool.to_a.should == [context]
69
+ end
70
+
71
+ it "should have the children of the context in the inactive_pool" do
72
+ subject.inactive_pool.to_a.should have(2).contexts
73
+ subject.inactive_pool.to_a.should == context.child_contexts
74
+ end
75
+ end
76
+
77
+
78
+ describe "#on_start_element" do
79
+ let(:context) do
80
+ Saxxy::ContextTree.new do
81
+ under("div", class: "foo") do
82
+ on("ul") {}
83
+ under("span", title: /bar/) {}
84
+ end
85
+
86
+ under("table") do
87
+ on("tr") {}
88
+ end
89
+ end.root
90
+ end
91
+ let(:subject) { klass.new(context) }
92
+
93
+ it "should not add any contexts to active pool if there is no context matching on the level" do
94
+ expect { subject.on_start_element("div4", class: "foo") }.to_not change { subject.active_pool.to_a }
95
+ end
96
+
97
+ it "should add the matching contexts to the active pool" do
98
+ subject.active_pool.to_a.should have(1).context
99
+ subject.on_start_element("div", class: "foo")
100
+ subject.active_pool.to_a.should have(2).contexts
101
+ subject.active_pool.to_a.last.activation_rule.element.should == "div"
102
+ end
103
+
104
+ it "should remove the matching contexts from tha inactive pool and add them to the active" do
105
+ matching = subject.inactive_pool.to_a.first
106
+ subject.on_start_element("div", class: "foo")
107
+ subject.inactive_pool.to_a.should_not include(matching)
108
+ subject.active_pool.to_a.should include(matching)
109
+ end
110
+
111
+ it "should add the child contexts of the matching ones to the inactive pool" do
112
+ expect { subject.on_start_element("div", class: "foo") }.to change {
113
+ subject.inactive_pool.to_a.map { |c| c.activation_rule.element }
114
+ }.from(["div", "table"]).to(["table", "span"])
115
+ end
116
+
117
+ it "should add the actions of the active contexts to the event registry" do
118
+ expect do
119
+ subject.on_start_element("div", class: "foo")
120
+ subject.on_start_element("ul", class: "whateva")
121
+ end.to change { actions.map { |a| a.activation_rule.element } }.from([]).to(["ul"])
122
+ end
123
+
124
+ it "should not add the actions of the inactive contexts to the registry" do
125
+ expect do
126
+ subject.on_start_element("table", class: "foo")
127
+ subject.on_start_element("tr", class: "whateva")
128
+ end.to_not change { actions.map { |a| a.activation_rule.element } }.from([]).to(["ul"])
129
+ end
130
+
131
+ it "should change the level on the activated contexts" do
132
+ matching = subject.inactive_pool.to_a.first
133
+ expect { subject.on_start_element("div", class: "foo") }.to change { get_level(matching) }.by(1)
134
+ end
135
+
136
+ it "should open the activated contexts" do
137
+ matching = subject.inactive_pool.to_a.first
138
+ expect do
139
+ subject.on_start_element("div", class: "foo")
140
+ end.to change { matching.send(:closed?) }.from(true).to(false)
141
+ end
142
+
143
+ it "should add an event if an action mathces the node" do
144
+ expect do
145
+ subject.on_start_element("div", class: "foo")
146
+ 3.times { subject.on_start_element("ul", class: "whateva") }
147
+ end.to change { registry.values.flatten.size }.by(3)
148
+ end
149
+
150
+ it "should change the level on the activated events if matches node" do
151
+ expect do
152
+ subject.on_start_element("div", class: "foo")
153
+ subject.on_start_element("ul", class: "whateva")
154
+ end.to change {
155
+ registry.values.first ? get_level(registry.values.first.last) : Saxxy::Activatable::DLEVEL_MIN
156
+ }.by(1)
157
+ end
158
+
159
+ it "should not change the level on previously activated events that match the node" do
160
+ subject.on_start_element("div", class: "foo")
161
+ subject.on_start_element("ul", class: "whateva")
162
+ previous = registry.values.first.last
163
+ expect { subject.on_start_element("ul", class: "whateva") }.to_not change { get_level(previous) }
164
+ end
165
+
166
+ it "should not change the level on previously activated events that do not match the node" do
167
+ subject.on_start_element("div", class: "foo")
168
+ subject.on_start_element("ul", class: "whateva")
169
+ previous = registry.values.first.last
170
+ expect { subject.on_start_element("ul2", class: "whateva") }.to_not change { get_level(previous) }
171
+ end
172
+
173
+ it "should call register_and_activate_events_on" do
174
+ subject.should_receive(:register_and_activate_events_on).with("ul", class: "foo")
175
+ subject.on_start_element("ul", class: "foo")
176
+ end
177
+
178
+ it "should call register_event_from_action on any matching action" do
179
+ subject.on_start_element("div", class: "foo")
180
+ subject.on_start_element("table", class: "foo")
181
+ subject.should have(2).actions
182
+ matching = subject.send(:actions).select { |a| a.matches("ul", class: "whateva") }.first
183
+ subject.should_receive(:register_event_from_action).with(matching, "ul", class: "whateva")
184
+ subject.on_start_element("ul", class: "whateva")
185
+ end
186
+
187
+ it "should call activate_events_on" do
188
+ subject.should_receive(:activate_events_on).with("ul", class: "foo")
189
+ subject.on_start_element("ul", class: "foo")
190
+ end
191
+
192
+ it "should call register_and_activate_events_on" do
193
+ subject.should_receive(:activate_contexts_on).with("ul", class: "foo")
194
+ subject.on_start_element("ul", class: "foo")
195
+ end
196
+ end
197
+
198
+
199
+ describe "#on_characters" do
200
+ let(:context) do
201
+ Saxxy::ContextTree.new do
202
+ under("div", class: "foo") do
203
+ on("ul") {}
204
+ on("span") {}
205
+ under("span", title: /bar/) {}
206
+ end
207
+
208
+ under("table") do
209
+ on("tr") {}
210
+ end
211
+ end.root
212
+ end
213
+ let(:subject) { klass.new(context) }
214
+
215
+ # Activate the first context and the first action
216
+ before do
217
+ subject.on_start_element("div", class: "foo")
218
+ subject.on_start_element("ul", {})
219
+ end
220
+
221
+ it "should add the text on the last activated event for the matching actions" do
222
+ expect do
223
+ subject.on_start_element("ul", {})
224
+ subject.on_characters("some chars")
225
+ end.to change { events.flatten.map(&:text) }.from([""]).to(["", "some chars"])
226
+ end
227
+
228
+ it "should append the text for as many times it is called" do
229
+ expect do
230
+ subject.on_characters("count")
231
+ 5.times { |i| subject.on_characters(" #{i+1}") }
232
+ end.to change { events.flatten.map(&:text) }.from([""]).to(["count 1 2 3 4 5"])
233
+ end
234
+
235
+ it "should append nothing if called with a nil argument" do
236
+ expect do
237
+ subject.on_characters(nil)
238
+ end.to_not change { events.flatten.map(&:text) }
239
+ end
240
+ end
241
+
242
+
243
+ describe "#on_end_element" do
244
+ let(:context) do
245
+ Saxxy::ContextTree.new do
246
+ under("div", class: "foo") do
247
+ on("ul") {}
248
+ on("span") {}
249
+ under("span", title: /bar/) {}
250
+ end
251
+
252
+ under("table") do
253
+ on("tr") {}
254
+ on("td") {}
255
+ end
256
+ end.root
257
+ end
258
+ let(:subject) { klass.new(context) }
259
+
260
+ context "events" do
261
+ # Activate an event
262
+ before do
263
+ subject.on_start_element("div", class: "foo")
264
+ subject.on_start_element("ul", {})
265
+ end
266
+
267
+ it "should remove the events that are closed from the registry" do
268
+ # Activate another event in order to have 2 events for that action
269
+ subject.on_start_element("ul", {})
270
+ events.flatten.should have(2).events
271
+ removed_event = events.flatten.last
272
+ removed_event.should_not be_closed
273
+ subject.on_end_element("ul")
274
+ removed_event.should be_closed
275
+ events.flatten.should_not include(removed_event)
276
+ end
277
+
278
+ it "should call the action of the closed events with the appended text" do
279
+ removed_event = events.flatten.last
280
+ removed_event.action.should_receive(:call).with("foo", "ul", {})
281
+ subject.on_characters("foo")
282
+ subject.on_end_element("ul")
283
+ removed_event.should be_closed
284
+ end
285
+
286
+ it "should not remove the still opened events" do
287
+ subject.on_start_element("ul", {})
288
+ events.flatten.should have(2).events
289
+ not_removed = events.flatten.first
290
+ subject.on_end_element("ul")
291
+ events.flatten.should include(not_removed)
292
+ end
293
+
294
+ # Note: If we assert that the action of the not removed event should not
295
+ # receive :call it will be a false assertion due to the fact that
296
+ # the events are pointing to the same action object.
297
+ it "should not call the action of the still opened events" do
298
+ subject.on_start_element("ul", {})
299
+ not_removed = events.flatten.first
300
+ not_removed.should_not_receive(:fire)
301
+ subject.on_end_element("ul")
302
+ not_removed.should_not be_closed
303
+ end
304
+ end
305
+
306
+ context "contexts" do
307
+ before { subject.on_start_element("table", class: "foo") }
308
+
309
+ it "should remove a closed context from the active pool" do
310
+ removed = pool.to_a.last
311
+ pool.to_a.should include(removed)
312
+ subject.on_end_element("table")
313
+ pool.to_a.should_not include(removed)
314
+ end
315
+
316
+ it "should not remove an open context from the active pool" do
317
+ not_removed = pool.last
318
+ pool.should include(not_removed)
319
+ subject.on_end_element("foo")
320
+ pool.should include(not_removed)
321
+ end
322
+
323
+ it "should not empty the active pool if there is other contexts in" do
324
+ subject.on_start_element("div", class: "foo")
325
+ subject.on_start_element("span", title: "foobar")
326
+ expect do
327
+ subject.on_end_element("span")
328
+ subject.on_end_element("div")
329
+ end.to change { pool.length }.from(4).to(2)
330
+ end
331
+ end
332
+
333
+
334
+ context "private methods" do
335
+ let(:context) do
336
+ Saxxy::ContextTree.new do
337
+ under("div", class: "foo") do
338
+ on("ul") {}
339
+ on("span") {}
340
+ under("span", title: /bar/) {}
341
+ end
342
+
343
+ under("table") do
344
+ on("tr") {}
345
+ on("td") {}
346
+ end
347
+ end.root
348
+ end
349
+ let(:subject) { klass.new(context) }
350
+
351
+
352
+ describe "#actions" do
353
+ before { subject.on_start_element("table", class: "foo") }
354
+
355
+ it "should contain the actions of the active contexts" do
356
+ subject.send(:actions).should == pool.last.actions
357
+ end
358
+
359
+ it "should not contain the actions of the inactive contexts" do
360
+ actions = subject.send(:actions)
361
+ actions.length.should_not be_zero
362
+ pool(:inactive).flat_map(&:actions).each do |action|
363
+ actions.should_not include(action)
364
+ end
365
+ end
366
+ end
367
+
368
+
369
+ describe "#on_remove_from_active_pool" do
370
+ # This will happen only if the html is malformed
371
+ it "should remove actions that are still in the registry" do
372
+ subject.on_start_element("table", class: "foo")
373
+ subject.on_start_element("tr", class: "foo")
374
+ expect do
375
+ subject.send(:on_remove_from_active_pool, pool.last)
376
+ end.to change { registry.values.empty? }.from(false).to(true)
377
+ end
378
+
379
+ it "should push the context back to the inactive pool if it's parent is in the active pool"
380
+ end
381
+
382
+
383
+ describe "#on_add_to_active_pool" do
384
+ it "will transfer it's child contexts into the inactive pool" do
385
+ div_context = context.child_contexts.first
386
+ div_context.child_contexts.should_not be_empty
387
+ div_context.child_contexts.each { |c| pool(:inactive).should_not include(c) }
388
+ subject.send(:on_add_to_active_pool, div_context)
389
+ div_context.child_contexts.each { |c| pool(:inactive).should include(c) }
390
+ end
391
+
392
+ it "should set the on_deactivation callback on the context" do
393
+ ctx = Saxxy::Context.new
394
+ ctx.should_receive(:on_deactivation)
395
+ subject.send(:on_add_to_active_pool, ctx)
396
+ end
397
+ end
398
+
399
+
400
+ describe "#on_add_to_inactive_pool" do
401
+ it "should set the on_activation callback on the context" do
402
+ ctx = Saxxy::Context.new
403
+ ctx.should_receive(:on_activation)
404
+ subject.send(:on_add_to_inactive_pool, ctx)
405
+ end
406
+ end
407
+
408
+
409
+ describe "#deactivate_contexts_on" do
410
+ it "should call deactivate_on on every context in active pool" do
411
+ subject.on_start_element("table")
412
+ pool.each { |c| c.should_receive(:deactivate_on).with("span") }
413
+ subject.send(:deactivate_contexts_on, "span")
414
+ end
415
+
416
+ it "should not call deactivate_on on contexts in inactive pool" do
417
+ subject.on_start_element("table")
418
+ pool(:inactive).each { |c| c.should_not_receive(:deactivate_on) }
419
+ subject.send(:deactivate_contexts_on, "span")
420
+ end
421
+ end
422
+
423
+
424
+ describe "#activate_contexts_on" do
425
+ it "should call deactivate_on on every context in active pool" do
426
+ subject.on_start_element("table")
427
+ pool(:inactive).each { |c| c.should_receive(:activate_on).with("span", class: "foo") }
428
+ subject.send(:activate_contexts_on, "span", class: "foo")
429
+ end
430
+
431
+ it "should not call deactivate_on on contexts in inactive pool" do
432
+ subject.on_start_element("table")
433
+ pool.each { |c| c.should_not_receive(:activate_on) }
434
+ subject.send(:activate_contexts_on, "span", class: "foo")
435
+ end
436
+ end
437
+
438
+
439
+ describe "#register_and_activate_events_on" do
440
+ it "should call register_event_from_action with any matching action as argument" do
441
+ a1, a2 = Object.new, Object.new
442
+ a1.should_receive(:matches).and_return(true)
443
+ a2.should_receive(:matches).and_return(false)
444
+ subject.stub(actions: [a1, a2])
445
+ subject.should_receive(:register_event_from_action).with(a1, "div", class: "foo")
446
+ subject.send(:register_and_activate_events_on, "div", class: "foo")
447
+ end
448
+
449
+ it "should call the activate_events_on with same arguments" do
450
+ subject.should_receive(:activate_events_on).with("div", class: "foo")
451
+ subject.send(:register_and_activate_events_on, "div", class: "foo")
452
+ end
453
+ end
454
+ end
455
+ end
456
+ end