hyper-spec 1.0.alpha1.3 → 1.0.alpha1.8

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,64 @@
1
+ # don't put this in directory lib/rspec/ as that will cause stack overflow with rails/rspec loads
2
+ module RSpec
3
+ module Expectations
4
+ class ExpectationTarget; end
5
+ module HyperSpecInstanceMethods
6
+ def self.included(base)
7
+ base.include HyperSpec::Helpers
8
+ end
9
+
10
+ def to_on_client(matcher, message = nil, &block)
11
+ evaluate_client.to(matcher, message, &block)
12
+ end
13
+
14
+ alias on_client_to to_on_client
15
+ alias to_then to_on_client
16
+ alias then_to to_on_client
17
+
18
+ def to_on_client_not(matcher, message = nil, &block)
19
+ evaluate_client.not_to(matcher, message, &block)
20
+ end
21
+
22
+ alias on_client_to_not to_on_client_not
23
+ alias on_client_not_to to_on_client_not
24
+ alias to_not_on_client to_on_client_not
25
+ alias not_to_on_client to_on_client_not
26
+ alias then_to_not to_on_client_not
27
+ alias then_not_to to_on_client_not
28
+ alias to_not_then to_on_client_not
29
+ alias not_to_then to_on_client_not
30
+
31
+ private
32
+
33
+ def evaluate_client
34
+ source = add_opal_block(@args_str, @target)
35
+ value = @target.binding.eval("evaluate_ruby(#{source.inspect}, {}, {})")
36
+ ExpectationTarget.for(value, nil)
37
+ end
38
+ end
39
+
40
+ class OnClientWithArgsTarget
41
+ include HyperSpecInstanceMethods
42
+
43
+ def initialize(target, args)
44
+ unless args.is_a? Hash
45
+ raise ExpectationNotMetError,
46
+ "You must pass a hash of local var, value pairs to the 'with' modifier"
47
+ end
48
+
49
+ @target = target
50
+ @args_str = args.collect do |name, value|
51
+ set_local_var(name, value)
52
+ end.join("\n")
53
+ end
54
+ end
55
+
56
+ class BlockExpectationTarget < ExpectationTarget
57
+ include HyperSpecInstanceMethods
58
+
59
+ def with(args)
60
+ OnClientWithArgsTarget.new(@target, args)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,225 @@
1
+ module HyperSpec
2
+ module Helpers
3
+ include Internal::ClientExecution
4
+ include Internal::Controller
5
+ include Internal::ComponentMount
6
+ include Internal::CopyLocals
7
+ include Internal::WindowSizing
8
+
9
+ ##
10
+ # Mount a component on a page, with a full execution environment
11
+ # i.e. `mount('MyComponent')` will mount MyComponent on the page.
12
+
13
+ # The params argument is a hash of parameters to be passed to the
14
+ # component.
15
+ # i.e. `mount('MyComponent', title: 'hello')`
16
+
17
+ # The options parameters can set things like:
18
+ # + controller: the controller class, defaults to HyperSpecTestController
19
+ # + no_wait: do not wait for any JS to finish executing before proceeding with the spec
20
+ # + render_on: :client_only (default), :client_and_server, or :server_only
21
+ # + style_sheet: style sheet file defaults to 'application'
22
+ # + javascript: javascript file defaults to 'application'
23
+ # + layout: if provided will use the specified layout otherwise no layout is used
24
+ # Note that if specifying options the params will have to inclosed in their
25
+ # own hash.
26
+ # i.e. `mount('MyComponent', { title: 'hello' }, render_on: :server_only)`
27
+ # The options can be specified globally using the client_options method (see below.)
28
+
29
+ # You may provide a block to mount. This block will be executed on the client
30
+ # before mounting the component. This is useful for setting up test
31
+ # components or modifying a components behavior.
32
+ # i.e.
33
+ # ```ruby
34
+ # mount('MyComponent', title: 'hello') do
35
+ # # this line will be printed on the client console
36
+ # puts "I'm about to mount my component!"
37
+ # end
38
+ # ```
39
+ def mount(component_name = nil, params = nil, opts = {}, &block)
40
+ unless params
41
+ params = opts
42
+ opts = {}
43
+ end
44
+ internal_mount(component_name, params, client_options(opts), &block)
45
+ end
46
+
47
+ ##
48
+ # The following methods retrieve callback and event responses from
49
+ # the mounted components. The history methods contain the array of all
50
+ # responses, while last_... returns the last response.
51
+ # i.e. event_history_for(:save) would return any save events
52
+ # that the component has raised.
53
+
54
+ %i[
55
+ callback_history_for last_callback_for clear_callback_history_for
56
+ event_history_for last_event_for clear_event_history_for
57
+ ].each do |method|
58
+ define_method(method) do |event_name|
59
+ evaluate_ruby(
60
+ "Hyperstack::Internal::Component::TopLevelRailsComponent.#{method}('#{event_name}')"
61
+ )
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Define a code block to be prefixed to the mount code.
67
+ # Useful in before(:each) blocks.
68
+
69
+ # In legacy code this was called `on_client`. To get the legacy
70
+ # behavior alias on_client before_mount
71
+ # but be aware that on_client is now by default the method
72
+ # for executing a block of code on the client which was called
73
+ # evaluate_ruby
74
+
75
+ def before_mount(&block)
76
+ @_hyperspec_private_client_code =
77
+ "#{@_hyperspec_private_client_code}#{add_opal_block('', block)}"
78
+ end
79
+
80
+ # Execute the block both on the client and on the server. Useful
81
+ # for mocking isomorphic classes such as ActiveRecord models.
82
+
83
+ def isomorphic(&block)
84
+ yield
85
+ if page.instance_variable_get('@hyper_spec_mounted')
86
+ internal_evaluate_ruby(&block)
87
+ else
88
+ before_mount(&block)
89
+ end
90
+ end
91
+
92
+ # Allows options to the mount method to be specified globally
93
+
94
+ def client_option(opts = {})
95
+ @_hyperspec_private_client_options ||= { arity_check: default_arity_check }
96
+ @_hyperspec_private_client_options.merge! opts
97
+ build_var_inclusion_lists
98
+ @_hyperspec_private_client_options
99
+ end
100
+
101
+ def default_arity_check
102
+ Rails.application.config.opal.arity_check_enabled if defined? Rails
103
+ rescue StandardError
104
+ false
105
+ end
106
+
107
+ alias client_options client_option
108
+
109
+ ##
110
+ # shorthand for mount with no params (which will effectively reload the page.)
111
+ # also aliased as reload_page
112
+ def load_page
113
+ mount
114
+ end
115
+
116
+ alias reload_page load_page
117
+
118
+ ##
119
+ # evaluate a block (or string) on the client
120
+ # on_client(<optional str>, <opts and/or vars>, &block)
121
+ #
122
+ # normal use is to pass a block that will be compiled to the client
123
+ # but if the ruby code can be supplied as a string in the first arg.
124
+
125
+ # opts are passed on to JSON.parse when retrieving the result
126
+ # from the client.
127
+
128
+ # vars is a hash of name: value pairs. Each name will be initialized
129
+ # as a local variable on the client.
130
+
131
+ # example: on_client(x: 12) { x * x } => 144
132
+
133
+ # in legacy code on_client was called before_mount
134
+ # to get legacy on_client behavior you can alias
135
+ # on_client before_mount
136
+
137
+ alias on_client internal_evaluate_ruby
138
+
139
+ # attempt to set the window to a particular size
140
+
141
+ def size_window(width = nil, height = nil)
142
+ hs_internal_resize_to(*determine_size(width, height))
143
+ rescue StandardError
144
+ true
145
+ end
146
+
147
+ # same signature as on_client, but just returns the compiled
148
+ # js code. Useful for debugging suspected issues with the
149
+ # Opal compiler, etc.
150
+
151
+ def to_js(*args, &block)
152
+ opal_compile(*process_params(*args, &block))
153
+ end
154
+
155
+ # legacy methods for backwards compatibility
156
+ # these may be removed in a future version
157
+
158
+ def expect_evaluate_ruby(*args, &block)
159
+ expect(evaluate_ruby(*args, &block))
160
+ end
161
+
162
+ alias evaluate_ruby internal_evaluate_ruby
163
+ alias evaluate_promise evaluate_ruby
164
+ alias expect_promise expect_evaluate_ruby
165
+
166
+ def run_on_client(&block)
167
+ script = opal_compile(Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last))
168
+ page.execute_script(script)
169
+ end
170
+
171
+ def insert_html(str)
172
+ @_hyperspec_private_html_block = "#{@_hyperspec_private_html_block}\n#{str}"
173
+ end
174
+
175
+ def ppr(str)
176
+ js = opal_compile(str)
177
+ execute_script("console.log(#{js})")
178
+ end
179
+
180
+ def debugger
181
+ `debugger`
182
+ nil
183
+ end
184
+
185
+ def add_class(class_name, style)
186
+ @_hyperspec_private_client_code =
187
+ "#{@_hyperspec_private_client_code}ComponentHelpers.add_class('#{class_name}', #{style})\n"
188
+ end
189
+
190
+ def attributes_on_client(model)
191
+ evaluate_ruby("#{model.class.name}.find(#{model.id}).attributes").symbolize_keys
192
+ end
193
+
194
+ ### --- Debugging Helpers ----
195
+
196
+ def pause(message = nil)
197
+ if message
198
+ puts message
199
+ internal_evaluate_ruby "puts #{message.inspect}.to_s + ' (type go() to continue)'"
200
+ end
201
+
202
+ page.evaluate_script('window.hyper_spec_waiting_for_go = true')
203
+
204
+ loop do
205
+ sleep 0.25
206
+ break unless page.evaluate_script('window.hyper_spec_waiting_for_go')
207
+ end
208
+ end
209
+
210
+ def open_in_chrome
211
+ # if ['linux', 'freebsd'].include?(`uname`.downcase)
212
+ # `google-chrome http://#{page.server.host}:#{page.server.port}#{page.current_path}`
213
+ # else
214
+ `open http://#{page.server.host}:#{page.server.port}#{page.current_path}`
215
+ # end
216
+
217
+ loop do
218
+ sleep 1.hour
219
+ end
220
+ end
221
+
222
+ # short hand for use in pry sessions
223
+ alias c? internal_evaluate_ruby
224
+ end
225
+ end
@@ -0,0 +1,94 @@
1
+ module HyperSpec
2
+ module Internal
3
+ module ClientExecution
4
+ def internal_evaluate_ruby(*args, &block)
5
+ insure_page_loaded
6
+ add_promise_execute_and_wait(*process_params(*args, &block))
7
+ end
8
+
9
+ private
10
+
11
+ def add_opal_block(str, block)
12
+ return str unless block
13
+
14
+ source = block.source
15
+ ast = Parser::CurrentRuby.parse(source)
16
+ ast = find_block(ast)
17
+ raise "could not find block within source: #{block.source}" unless ast
18
+
19
+ "#{add_locals(str, block)}\n#{Unparser.unparse ast.children.last}"
20
+ end
21
+
22
+ def add_promise_execute_and_wait(str, opts)
23
+ js = opal_compile(add_promise_wrapper(str))
24
+ page.execute_script("window.hyper_spec_promise_result = false; #{js}")
25
+ Timeout.timeout(Capybara.default_max_wait_time) do
26
+ loop do
27
+ break if page.evaluate_script('!!window.hyper_spec_promise_result')
28
+ page.evaluate_script('!!window.hyper_spec_promise_failed && Opal.Opal.$raise(window.hyper_spec_promise_failed)')
29
+
30
+ sleep 0.25
31
+ end
32
+ end
33
+ JSON.parse(page.evaluate_script('window.hyper_spec_promise_result.$to_json()'), opts).first
34
+ end
35
+
36
+ def add_promise_wrapper(str)
37
+ <<~RUBY
38
+ (#{str}).tap do |r|
39
+ if defined?(Promise) && r.is_a?(Promise)
40
+ r.then { |args| `window.hyper_spec_promise_result = [args]` }
41
+ .fail { |e| `window.hyper_spec_promise_failed = e` }
42
+ else
43
+ #after(0) do
44
+ #puts "setting window.hyper_spec_promise_result = [\#{r}]"
45
+ `window.hyper_spec_promise_result = [r]`
46
+ #end
47
+ end
48
+ end
49
+ RUBY
50
+ end
51
+
52
+ def find_block(node)
53
+ # find a block with the ast tree.
54
+
55
+ return false unless node.class == Parser::AST::Node
56
+ return node if the_node_you_are_looking_for?(node)
57
+
58
+ node.children.each do |child|
59
+ found = find_block(child)
60
+ return found if found
61
+ end
62
+ false
63
+ end
64
+
65
+ def process_params(*args, &block)
66
+ args = ['', *args] if args[0].is_a? Hash
67
+ args = [args[0], {}, args[1] || {}] if args.length < 3
68
+ str, opts, vars = args
69
+ vars.each do |name, value|
70
+ str = "#{name} = #{value.inspect}\n#{str}"
71
+ end
72
+ [add_opal_block(str, block), opts]
73
+ end
74
+
75
+ def the_node_you_are_looking_for?(node)
76
+ # we could also check that the block is going to the right method
77
+ # respond_to?(node.children.first.children[1]) &&
78
+ # method(node.children.first.children[1]) == method(:evaluate_ruby)
79
+ # however that does not work for expect { ... }.on_client_to ...
80
+ # because now the block is being sent to expect... so we could
81
+ # check the above OR node.children.first.children[1] == :expect
82
+ # but what if there are two blocks? on and on...
83
+ node.type == :block &&
84
+ node.children.first.class == Parser::AST::Node &&
85
+ node.children.first.type == :send
86
+ end
87
+
88
+
89
+ def opal_compile(str)
90
+ Opal.hyperspec_compile(str, arity_check: client_options[:arity_check])
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,140 @@
1
+ module HyperSpec
2
+ module Internal
3
+ module ComponentMount
4
+ private
5
+
6
+ TEST_CODE_KEY = 'hyper_spec_prerender_test_code.js'.freeze
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def add_block_with_helpers(component_name, opts, block)
10
+ return unless block || @_hyperspec_private_client_code || component_name.nil?
11
+
12
+ block_with_helpers = <<-RUBY
13
+ module ComponentHelpers
14
+ def self.js_eval(s)
15
+ `eval(s)`
16
+ end
17
+ def self.dasherize(s)
18
+ res = %x{
19
+ s.replace(/[-_\\s]+/g, '-')
20
+ .replace(/([A-Z\\d]+)([A-Z][a-z])/g, '$1-$2')
21
+ .replace(/([a-z\\d])([A-Z])/g, '$1-$2')
22
+ .toLowerCase()
23
+ }
24
+ res
25
+ end
26
+ def self.add_class(class_name, styles={})
27
+ style = styles.collect { |attr, value| "\#{dasherize(attr)}:\#{value}" }.join("; ")
28
+ cs = class_name.to_s
29
+ %x{
30
+ var style_el = document.createElement("style");
31
+ var css = "." + cs + " { " + style + " }";
32
+ style_el.type = "text/css";
33
+ if (style_el.styleSheet){
34
+ style_el.styleSheet.cssText = css;
35
+ } else {
36
+ style_el.appendChild(document.createTextNode(css));
37
+ }
38
+ document.head.appendChild(style_el);
39
+ }
40
+ end
41
+ end
42
+ #{test_dummy}
43
+ #{@_hyperspec_private_client_code}
44
+ #{"#{add_locals('', block)}\n#{Unparser.unparse(Parser::CurrentRuby.parse(block.source).children.last)}" if block}
45
+ RUBY
46
+ @_hyperspec_private_client_code = nil
47
+ opts[:code] = opal_compile(block_with_helpers)
48
+ end
49
+ # rubocop:enable Metrics/MethodLength
50
+
51
+ def build_test_url_for(controller = nil, ping = nil)
52
+ id = ping ? 'ping' : Controller.test_id
53
+ "/#{route_root_for(controller)}/#{id}"
54
+ end
55
+
56
+ def insure_page_loaded(only_if_code_or_html_exists = nil)
57
+ return if only_if_code_or_html_exists && !@_hyperspec_private_client_code && !@_hyperspec_private_html_block
58
+
59
+ # if we are not resetting between examples, or think its mounted
60
+ # then look for Opal, but if we can't find it, then ping to clear and try again
61
+ if !HyperSpec.reset_between_examples? || page.instance_variable_get('@hyper_spec_mounted')
62
+ r = evaluate_script('Opal && true') rescue nil
63
+ return if r
64
+
65
+ page.visit build_test_url_for(nil, true) rescue nil
66
+ end
67
+ load_page
68
+ end
69
+
70
+ def internal_mount(component_name, params, opts, &block)
71
+ # TODO: refactor this
72
+ test_url = build_test_url_for(opts.delete(:controller))
73
+ add_block_with_helpers(component_name, opts, block)
74
+ send_params_to_controller_via_cache(test_url, component_name, params, opts)
75
+ setup_prerendering(opts)
76
+ page.instance_variable_set('@hyper_spec_mounted', false)
77
+ visit test_url
78
+ wait_for_ajax unless opts[:no_wait]
79
+ page.instance_variable_set('@hyper_spec_mounted', true)
80
+ Lolex.init(self, client_options[:time_zone], client_options[:clock_resolution])
81
+ end
82
+
83
+ def prerendering?(opts)
84
+ %i[both server_only].include?(opts[:render_on])
85
+ end
86
+
87
+ def send_params_to_controller_via_cache(test_url, component_name, params, opts)
88
+ component_name ||= 'Hyperstack::Internal::Component::TestDummy' if test_dummy
89
+ Controller.cache_write(
90
+ test_url,
91
+ [component_name, params, @_hyperspec_private_html_block, opts]
92
+ )
93
+ @_hyperspec_private_html_block = nil
94
+ end
95
+
96
+ # test_code_key = "hyper_spec_prerender_test_code.js"
97
+ # if defined? ::Hyperstack::Component
98
+ # @@original_server_render_files ||= ::Rails.configuration.react.server_renderer_options[:files]
99
+ # if opts[:render_on] == :both || opts[:render_on] == :server_only
100
+ # unless opts[:code].blank?
101
+ # ComponentTestHelpers.cache_write(test_code_key, opts[:code])
102
+ # ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files + [test_code_key]
103
+ # ::React::ServerRendering.reset_pool # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher doesnt look for cache changes
104
+ # else
105
+ # ComponentTestHelpers.cache_delete(test_code_key)
106
+ # ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files
107
+ # ::React::ServerRendering.reset_pool # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher doesnt look for cache changes
108
+ # end
109
+ # end
110
+ # end
111
+
112
+ def setup_prerendering(opts)
113
+ return unless defined?(::Hyperstack::Component) && prerendering?(opts)
114
+
115
+ @@original_server_render_files ||= ::Rails.configuration.react.server_renderer_options[:files]
116
+ ::Rails.configuration.react.server_renderer_options[:files] = @@original_server_render_files
117
+ if opts[:code].blank?
118
+ Controller.cache_delete(TEST_CODE_KEY)
119
+ else
120
+ Controller.cache_write(TEST_CODE_KEY, opts[:code])
121
+ ::Rails.configuration.react.server_renderer_options[:files] += [TEST_CODE_KEY]
122
+ end
123
+ ::React::ServerRendering.reset_pool
124
+ # make sure contexts are reloaded so they dont use code from cache, as the rails filewatcher
125
+ # doesnt look for cache changes
126
+ end
127
+
128
+ def test_dummy
129
+ return unless defined? ::Hyperstack::Component
130
+
131
+ <<-RUBY
132
+ class Hyperstack::Internal::Component::TestDummy
133
+ include Hyperstack::Component
134
+ render {}
135
+ end
136
+ RUBY
137
+ end
138
+ end
139
+ end
140
+ end