hyper-spec 0.1.1 → 0.1.2

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.
@@ -0,0 +1,306 @@
1
+ # see component_test_helpers_spec.rb for examples
2
+
3
+ require 'parser/current'
4
+ require 'unparser'
5
+ require 'method_source'
6
+ require_relative '../../vendor/assets/javascripts/time_cop' # 'hyper-spec/time_cop'
7
+
8
+ module HyperSpec
9
+ module ComponentTestHelpers
10
+ TOP_LEVEL_COMPONENT_PATCH =
11
+ Opal.compile(File.read(File.expand_path('../../react/top_level_rails_component.rb', __FILE__)))
12
+
13
+ class << self
14
+ attr_accessor :current_example
15
+ attr_accessor :description_displayed
16
+
17
+ def display_example_description
18
+ "<script type='text/javascript'>console.log(console.log('%c#{current_example.description}'"\
19
+ ",'color:green; font-weight:bold; font-size: 200%'))</script>"
20
+ end
21
+ end
22
+
23
+ def build_test_url_for(controller)
24
+ unless controller
25
+ unless defined?(::ReactTestController)
26
+ Object.const_set('ReactTestController', Class.new(ActionController::Base))
27
+ end
28
+
29
+ controller = ::ReactTestController
30
+ end
31
+
32
+ route_root = controller.name.gsub(/Controller$/, '').underscore
33
+
34
+ unless controller.method_defined?(:test)
35
+ controller.class_eval do
36
+ define_method(:test) do
37
+ route_root = self.class.name.gsub(/Controller$/, '').underscore
38
+ test_params = ::Rails.cache.read("/#{route_root}/#{params[:id]}")
39
+ @component_name = test_params[0]
40
+ @component_params = test_params[1]
41
+ render_params = test_params[2]
42
+ render_on = render_params.delete(:render_on) || :client_only
43
+ _mock_time = render_params.delete(:mock_time)
44
+ style_sheet = render_params.delete(:style_sheet)
45
+ javascript = render_params.delete(:javascript)
46
+ code = render_params.delete(:code)
47
+
48
+ page = '<%= react_component @component_name, @component_params, '\
49
+ "{ prerender: #{render_on != :client_only} } %>"
50
+ page = "<script type='text/javascript'>\n#{TOP_LEVEL_COMPONENT_PATCH}\n</script>\n#{page}"
51
+
52
+ page = "<script type='text/javascript'>\n#{code}\n</script>\n#{page}" if code
53
+
54
+ page = "<%= javascript_include_tag 'time_cop' %>\n#{page}" if true || Lolex.initialized?
55
+
56
+ if (render_on != :server_only && !render_params[:layout]) || javascript
57
+ page = "<%= javascript_include_tag '#{javascript || 'application'}' %>\n#{page}"
58
+ end
59
+
60
+ if !render_params[:layout] || style_sheet
61
+ page = "<%= stylesheet_link_tag '#{style_sheet || 'application'}' %>\n#{page}"
62
+ end
63
+
64
+ if render_on == :server_only # so that test helper wait_for_ajax works
65
+ page = "<script type='text/javascript'>window.jQuery = {'active': 0}</script>\n#{page}"
66
+ else
67
+ page = "<%= javascript_include_tag 'jquery' %>\n"\
68
+ "<%= javascript_include_tag 'jquery_ujs' %>\n#{page}"
69
+ end
70
+
71
+ page = "<script type='text/javascript'>go = function() "\
72
+ "{window.hyper_spec_waiting_for_go = false}</script>\n#{page}"
73
+
74
+ title = view_context.escape_javascript(ComponentTestHelpers.current_example.description)
75
+ title = "#{title}...continued." if ComponentTestHelpers.description_displayed
76
+
77
+ page = "<script type='text/javascript'>console.log(console.log('%c#{title}',"\
78
+ "'color:green; font-weight:bold; font-size: 200%'))</script>\n#{page}"
79
+
80
+ ComponentTestHelpers.description_displayed = true
81
+ render_params[:inline] = page
82
+ render render_params
83
+ end
84
+ end
85
+
86
+ begin
87
+ routes = ::Rails.application.routes
88
+ routes.disable_clear_and_finalize = true
89
+ routes.clear!
90
+ routes.draw do
91
+ get "/#{route_root}/:id", to: "#{route_root}#test"
92
+ end
93
+ ::Rails.application.routes_reloader.paths.each { |path| load(path) }
94
+ routes.finalize!
95
+ ActiveSupport.on_load(:action_controller) { routes.finalize! }
96
+ ensure
97
+ routes.disable_clear_and_finalize = false
98
+ end
99
+ end
100
+
101
+ "/#{route_root}/#{@test_id = (@test_id || 0) + 1}"
102
+ end
103
+
104
+ def isomorphic(&block)
105
+ yield
106
+ on_client(&block)
107
+ end
108
+
109
+ def evaluate_ruby(str = '', opts = {}, &block)
110
+ insure_mount
111
+ if block
112
+ str = "#{str}\n#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}"
113
+ end
114
+ js = Opal.compile(str).delete("\n").gsub('(Opal);', '(Opal)')
115
+ JSON.parse(evaluate_script("[#{js}].$to_json()"), opts).first
116
+ end
117
+
118
+ def expect_evaluate_ruby(str = '', opts = {}, &block)
119
+ insure_mount
120
+ expect(evaluate_ruby(add_opal_block(str, block), opts))
121
+ end
122
+
123
+ def add_opal_block(str, block)
124
+ # big assumption here is that we are going to follow this with a .to
125
+ # hence .children.first followed by .children.last
126
+ # probably should do some kind of "search" to make this work nicely
127
+ return str unless block
128
+ "#{str}\n"\
129
+ "#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.first.children.last}"
130
+ end
131
+
132
+ def expect_promise(str = '', opts = {}, &block)
133
+ insure_mount
134
+
135
+ str = add_opal_block(str, block)
136
+ str = "#{str}.then { |args| args = [args]; `window.hyper_spec_promise_result = args` }"
137
+ js = Opal.compile(str).delete("\n").gsub('(Opal);', '(Opal)')
138
+ page.evaluate_script('window.hyper_spec_promise_result = false')
139
+ page.execute_script(js)
140
+
141
+ Timeout.timeout(Capybara.default_max_wait_time) do
142
+ loop do
143
+ sleep 0.25
144
+ break if page.evaluate_script('!!window.hyper_spec_promise_result')
145
+ end
146
+ end
147
+
148
+ result =
149
+ JSON.parse(page.evaluate_script('window.hyper_spec_promise_result.$to_json()'), opts).first
150
+ expect(result)
151
+ end
152
+
153
+ def ppr(str)
154
+ js = Opal.compile(str).delete("\n").gsub('(Opal);', '(Opal)')
155
+ execute_script("console.log(#{js})")
156
+ end
157
+
158
+ def on_client(&block)
159
+ @client_code =
160
+ "#{@client_code}#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}\n"
161
+ end
162
+
163
+ def debugger
164
+ `debugger`
165
+ nil
166
+ end
167
+
168
+ def insure_mount
169
+ # rescue in case page is not defined...
170
+ mount unless page.instance_variable_get('@hyper_spec_mounted')
171
+ end
172
+
173
+ def client_option(opts = {})
174
+ @client_options ||= {}
175
+ @client_options.merge! opts
176
+ end
177
+
178
+ alias client_options client_option
179
+
180
+ def mount(component_name = nil, params = nil, opts = {}, &block)
181
+ unless params
182
+ params = opts
183
+ opts = {}
184
+ end
185
+
186
+ opts = client_options opts
187
+ test_url = build_test_url_for(opts.delete(:controller))
188
+
189
+ if block || @client_code || component_name.nil?
190
+ block_with_helpers = <<-code
191
+ module ComponentHelpers
192
+ def self.js_eval(s)
193
+ `eval(s)`
194
+ end
195
+ def self.dasherize(s)
196
+ `s.replace(/[-_\\s]+/g, '-')
197
+ .replace(/([A-Z\\d]+)([A-Z][a-z])/g, '$1-$2')
198
+ .replace(/([a-z\\d])([A-Z])/g, '$1-$2')
199
+ .toLowerCase()`
200
+ end
201
+ def self.add_class(class_name, styles={})
202
+ style = styles.collect { |attr, value| "\#{dasherize(attr)}:\#{value}"}.join("; ")
203
+ s = "<style type='text/css'> .\#{class_name}{ \#{style} } </style>"
204
+ `$(\#{s}).appendTo("head");`
205
+ end
206
+ end
207
+ class React::Component::HyperTestDummy < React::Component::Base
208
+ def render; end
209
+ end
210
+ #{@client_code}
211
+ #{Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last) if block}
212
+ code
213
+ opts[:code] = Opal.compile(block_with_helpers)
214
+ end
215
+
216
+ component_name ||= 'React::Component::HyperTestDummy'
217
+ ::Rails.cache.write(test_url, [component_name, params, opts])
218
+ visit test_url
219
+ wait_for_ajax unless opts[:no_wait]
220
+ page.instance_variable_set('@hyper_spec_mounted', true)
221
+ Lolex.init(self, client_options[:time_zone], client_options[:clock_resolution])
222
+ end
223
+
224
+ [:callback_history_for, :last_callback_for, :clear_callback_history_for,
225
+ :event_history_for, :last_event_for, :clear_event_history_for].each do |method|
226
+ define_method(method) do |event_name|
227
+ evaluate_ruby("React::TopLevelRailsComponent.#{method}('#{event_name}')")
228
+ end
229
+ end
230
+
231
+ def run_on_client(&block)
232
+ script = Opal.compile(Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last))
233
+ execute_script(script)
234
+ end
235
+
236
+ def add_class(class_name, style)
237
+ @client_code = "#{@client_code}ComponentHelpers.add_class '#{class_name}', #{style}\n"
238
+ end
239
+
240
+ def open_in_chrome
241
+ if false && ['linux', 'freebsd'].include?(`uname`.downcase)
242
+ `google-chrome http://#{page.server.host}:#{page.server.port}#{page.current_path}`
243
+ else
244
+ `open http://#{page.server.host}:#{page.server.port}#{page.current_path}`
245
+ end
246
+
247
+ while true
248
+ sleep 1.hour
249
+ end
250
+ end
251
+
252
+ def pause(message = nil)
253
+ if message
254
+ puts message
255
+ page.evaluate_ruby "puts #{message.inspect}.to_s + ' (type go() to continue)'"
256
+ end
257
+
258
+ page.evaluate_script('window.hyper_spec_waiting_for_go = true')
259
+
260
+ loop do
261
+ sleep 0.25
262
+ break unless page.evaluate_script('window.hyper_spec_waiting_for_go')
263
+ end
264
+ end
265
+
266
+ def size_window(width = nil, height = nil)
267
+ width, height = [height, width] if width == :portrait
268
+ width, height = width if width.is_a? Array
269
+ portrait = true if height == :portrait
270
+
271
+ case width
272
+ when :small
273
+ width, height = [480, 320]
274
+ when :mobile
275
+ width, height = [640, 480]
276
+ when :tablet
277
+ width, height = [960, 640]
278
+ when :large
279
+ width, height = [1920, 6000]
280
+ when :default, nil
281
+ width, height = [1024, 768]
282
+ end
283
+
284
+ width, height = [height, width] if portrait
285
+
286
+ Capybara.current_session.current_window.resize_to(width, height)
287
+ end
288
+ end
289
+
290
+ RSpec.configure do |config|
291
+ config.before(:each) do |example|
292
+ ComponentTestHelpers.current_example = example
293
+ ComponentTestHelpers.description_displayed = false
294
+ end
295
+
296
+ if defined?(ActiveRecord)
297
+ config.before(:all) do
298
+ ActiveRecord::Base.class_eval do
299
+ def attributes_on_client(page)
300
+ page.evaluate_ruby("#{self.class.name}.find(#{id}).attributes", symbolize_names: true)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,66 @@
1
+ module HyperSpec
2
+ class Lolex
3
+ class << self
4
+ def init(page, client_time_zone, resolution)
5
+ @capybara_page = page
6
+ @resolution = resolution || 10
7
+ @client_time_zone = client_time_zone
8
+ run_pending_evaluations
9
+ @initialized = true
10
+ end
11
+
12
+ def initialized?
13
+ @initialized
14
+ end
15
+
16
+ def push(mock_type, *args)
17
+ scale = if mock_type == :freeze
18
+ 0
19
+ elsif mock_type == :scale
20
+ args[0]
21
+ else
22
+ 1
23
+ end
24
+ evaluate_ruby do
25
+ "Lolex.push('#{time_string_in_zone}', #{scale}, #{@resolution})"
26
+ end
27
+ end
28
+
29
+ def pop
30
+ evaluate_ruby { 'Lolex.pop' }
31
+ end
32
+
33
+ def unmock
34
+ evaluate_ruby { "Lolex.unmock('#{time_string_in_zone}', #{@resolution})" }
35
+ end
36
+
37
+ def restore
38
+ evaluate_ruby { 'Lolex.restore' }
39
+ end
40
+
41
+ private
42
+
43
+ def time_string_in_zone
44
+ Time.now.in_time_zone(@client_time_zone).strftime('%Y/%m/%d %H:%M:%S %z')
45
+ end
46
+
47
+ def pending_evaluations
48
+ @pending_evaluations ||= []
49
+ end
50
+
51
+ def evaluate_ruby(&block)
52
+ if @capybara_page
53
+ @capybara_page.evaluate_ruby(yield)
54
+ else
55
+ pending_evaluations << block
56
+ end
57
+ end
58
+
59
+ def run_pending_evaluations
60
+ return if pending_evaluations.empty?
61
+ @capybara_page.evaluate_ruby(pending_evaluations.collect(&:call).join("\n"))
62
+ @pending_evaluations ||= []
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,4 +1,5 @@
1
1
  require 'rails'
2
+
2
3
  module HyperSpec
3
4
  module Rails
4
5
  class Engine < ::Rails::Engine
@@ -1,113 +1,44 @@
1
- # Interface to the Lolex package running on the client side
2
- # Below we will monkey patch Timecop to call these methods
3
- class Lolex
4
- class << self
5
- def init(page, client_time_zone, resolution)
6
- @capybara_page = page
7
- @resolution = resolution || 10
8
- @client_time_zone = client_time_zone
9
- run_pending_evaluations
10
- @initialized = true
11
- end
12
-
13
- def initialized?
14
- @initialized
15
- end
16
-
17
- def push(mock_type, *args)
18
- scale = if mock_type == :freeze
19
- 0
20
- elsif mock_type == :scale
21
- args[0]
22
- else
23
- 1
24
- end
25
- evaluate_ruby do
26
- "Lolex.push('#{time_string_in_zone}', #{scale}, #{@resolution})"
27
- end
28
- end
29
-
30
- def pop
31
- evaluate_ruby { 'Lolex.pop' }
32
- end
1
+ require 'timecop'
33
2
 
34
- def unmock
35
- evaluate_ruby { "Lolex.unmock('#{time_string_in_zone}', #{@resolution})" }
36
- end
3
+ module HyperSpec
4
+ class Timecop
5
+ private
37
6
 
38
- def restore
39
- evaluate_ruby { 'Lolex.restore' }
40
- end
7
+ def travel(mock_type, *args, &block)
8
+ raise SafeModeException if Timecop.safe_mode? && !block_given?
41
9
 
42
- private
10
+ stack_item = TimeStackItem.new(mock_type, *args)
43
11
 
44
- def time_string_in_zone
45
- Time.now.in_time_zone(@client_time_zone).strftime('%Y/%m/%d %H:%M:%S %z')
46
- end
12
+ stack_backup = @_stack.dup
13
+ @_stack << stack_item
47
14
 
48
- def pending_evaluations
49
- @pending_evaluations ||= []
50
- end
15
+ Lolex.push(mock_type, *args)
51
16
 
52
- def evaluate_ruby(&block)
53
- if @capybara_page
54
- @capybara_page.evaluate_ruby(yield)
55
- else
56
- pending_evaluations << block
17
+ if block_given?
18
+ begin
19
+ yield stack_item.time
20
+ ensure
21
+ Lolex.pop
22
+ @_stack.replace stack_backup
23
+ end
57
24
  end
58
25
  end
59
26
 
60
- def run_pending_evaluations
61
- return if pending_evaluations.empty?
62
- @capybara_page.evaluate_ruby(pending_evaluations.collect do |block|
63
- block.call
64
- end.join("\n"))
65
- @pending_evaluations ||= []
27
+ def return(&block)
28
+ current_stack = @_stack
29
+ current_baseline = @baseline
30
+ unmock!
31
+ yield
32
+ ensure
33
+ Lolex.restore
34
+ @_stack = current_stack
35
+ @baseline = current_baseline
66
36
  end
67
- end
68
- end
69
-
70
- require 'timecop'
71
-
72
- # Monkey patches to call our Lolex interface
73
- class Timecop
74
-
75
- private
76
-
77
- def travel(mock_type, *args, &block)
78
- raise SafeModeException if Timecop.safe_mode? && !block_given?
79
-
80
- stack_item = TimeStackItem.new(mock_type, *args)
81
37
 
82
- stack_backup = @_stack.dup
83
- @_stack << stack_item
84
-
85
- Lolex.push(mock_type, *args)
86
-
87
- if block_given?
88
- begin
89
- yield stack_item.time
90
- ensure
91
- Lolex.pop
92
- @_stack.replace stack_backup
93
- end
38
+ def unmock! #:nodoc:
39
+ @baseline = nil
40
+ @_stack = []
41
+ Lolex.unmock
94
42
  end
95
43
  end
96
-
97
- def return(&block)
98
- current_stack = @_stack
99
- current_baseline = @baseline
100
- unmock!
101
- yield
102
- ensure
103
- Lolex.restore
104
- @_stack = current_stack
105
- @baseline = current_baseline
106
- end
107
-
108
- def unmock! #:nodoc:
109
- @baseline = nil
110
- @_stack = []
111
- Lolex.unmock
112
- end
113
44
  end