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.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/hyper-spec.gemspec +17 -20
- data/lib/hyper-spec.rb +170 -28
- data/lib/hyper-spec/controller_helpers.rb +164 -0
- data/lib/hyper-spec/expectations.rb +64 -0
- data/lib/hyper-spec/helpers.rb +225 -0
- data/lib/hyper-spec/internal/client_execution.rb +94 -0
- data/lib/hyper-spec/internal/component_mount.rb +140 -0
- data/lib/hyper-spec/internal/controller.rb +70 -0
- data/lib/hyper-spec/internal/copy_locals.rb +103 -0
- data/lib/hyper-spec/internal/patches.rb +86 -0
- data/lib/hyper-spec/internal/rails_controller_helpers.rb +50 -0
- data/lib/hyper-spec/{time_cop.rb → internal/time_cop.rb} +14 -2
- data/lib/hyper-spec/internal/window_sizing.rb +73 -0
- data/lib/hyper-spec/rack.rb +67 -0
- data/lib/hyper-spec/version.rb +1 -1
- data/lib/hyper-spec/wait_for_ajax.rb +1 -1
- data/multi_level_how_it_works.md +49 -0
- metadata +100 -69
- data/Gemfile.lock +0 -373
- data/lib/hyper-spec/component_test_helpers.rb +0 -362
- data/lib/hyper-spec/unparser_patch.rb +0 -10
@@ -0,0 +1,70 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module Controller
|
4
|
+
class << self
|
5
|
+
attr_accessor :current_example
|
6
|
+
attr_accessor :description_displayed
|
7
|
+
|
8
|
+
def test_id
|
9
|
+
@_hyperspec_private_test_id ||= 0
|
10
|
+
@_hyperspec_private_test_id += 1
|
11
|
+
end
|
12
|
+
|
13
|
+
include ActionView::Helpers::JavaScriptHelper
|
14
|
+
|
15
|
+
def current_example_description!
|
16
|
+
title = "#{title}...continued." if description_displayed
|
17
|
+
self.description_displayed = true
|
18
|
+
"#{escape_javascript(current_example.description)}#{title}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def file_cache
|
22
|
+
@file_cache ||= FileCache.new('cache', '/tmp/hyper-spec-caches', 30, 3)
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_read(key)
|
26
|
+
file_cache.get(key)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cache_write(key, value)
|
30
|
+
file_cache.set(key, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache_delete(key)
|
34
|
+
file_cache.delete(key)
|
35
|
+
rescue StandardError
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# By default we assume we are operating in a Rails environment and will
|
41
|
+
# hook in using a rails controller. To override this define the
|
42
|
+
# HyperSpecController class in your spec helper. See the rack.rb file
|
43
|
+
# for an example of how to do this.
|
44
|
+
|
45
|
+
def hyper_spec_test_controller
|
46
|
+
return ::HyperSpecTestController if defined?(::HyperSpecTestController)
|
47
|
+
|
48
|
+
base = if defined? ApplicationController
|
49
|
+
Class.new ApplicationController
|
50
|
+
elsif defined? ::ActionController::Base
|
51
|
+
Class.new ::ActionController::Base
|
52
|
+
else
|
53
|
+
raise "Unless using Rails you must define the HyperSpecTestController\n"\
|
54
|
+
'For rack apps try requiring hyper-spec/rack.'
|
55
|
+
end
|
56
|
+
Object.const_set('HyperSpecTestController', base)
|
57
|
+
end
|
58
|
+
|
59
|
+
# First insure we have a controller, then make sure it responds to the test method
|
60
|
+
# if not, then add the rails specific controller methods. The RailsControllerHelpers
|
61
|
+
# module will automatically add a top level route back to the controller.
|
62
|
+
|
63
|
+
def route_root_for(controller)
|
64
|
+
controller ||= hyper_spec_test_controller
|
65
|
+
controller.include RailsControllerHelpers unless controller.method_defined?(:test)
|
66
|
+
controller.route_root
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module CopyLocals
|
4
|
+
private
|
5
|
+
|
6
|
+
def build_var_inclusion_lists
|
7
|
+
build_included_list
|
8
|
+
build_excluded_list
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_included_list
|
12
|
+
@_hyperspec_private_included_vars = nil
|
13
|
+
return unless @_hyperspec_private_client_options.key? :include_vars
|
14
|
+
|
15
|
+
included = @_hyperspec_private_client_options[:include_vars]
|
16
|
+
if included.is_a? Symbol
|
17
|
+
@_hyperspec_private_included_vars = [included]
|
18
|
+
elsif included.is_a?(Array)
|
19
|
+
@_hyperspec_private_included_vars = included
|
20
|
+
elsif !included
|
21
|
+
@_hyperspec_private_included_vars = []
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
PRIVATE_VARIABLES = %i[
|
26
|
+
@__inspect_output @__memoized @example @_hyperspec_private_client_code
|
27
|
+
@_hyperspec_private_html_block @fixture_cache
|
28
|
+
@fixture_connections @connection_subscriber @loaded_fixtures
|
29
|
+
@_hyperspec_private_client_options
|
30
|
+
@_hyperspec_private_included_vars
|
31
|
+
@_hyperspec_private_excluded_vars
|
32
|
+
b __ _ _ex_ pry_instance _out_ _in_ _dir_ _file_
|
33
|
+
]
|
34
|
+
|
35
|
+
def build_excluded_list
|
36
|
+
return unless @_hyperspec_private_client_options
|
37
|
+
|
38
|
+
excluded = @_hyperspec_private_client_options[:exclude_vars]
|
39
|
+
if excluded.is_a? Symbol
|
40
|
+
@_hyperspec_private_excluded_vars = [excluded]
|
41
|
+
elsif excluded.is_a?(Array)
|
42
|
+
@_hyperspec_private_excluded_vars = excluded
|
43
|
+
elsif excluded
|
44
|
+
@_hyperspec_private_included_vars = []
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def var_excluded?(var, binding)
|
49
|
+
return true if PRIVATE_VARIABLES.include? var
|
50
|
+
|
51
|
+
excluded = binding.eval('instance_variable_get(:@_hyperspec_private_excluded_vars)')
|
52
|
+
return true if excluded&.include?(var)
|
53
|
+
|
54
|
+
included = binding.eval('instance_variable_get(:@_hyperspec_private_included_vars)')
|
55
|
+
included && !included.include?(var)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_locals(in_str, block)
|
59
|
+
b = block.binding
|
60
|
+
add_instance_vars(b, add_local_vars(b, add_memoized_vars(b, in_str)))
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_memoized_vars(binding, in_str)
|
64
|
+
memoized = binding.eval('__memoized').instance_variable_get(:@memoized)
|
65
|
+
return in_str unless memoized
|
66
|
+
|
67
|
+
memoized.inject(in_str) do |str, pair|
|
68
|
+
next str if var_excluded?(pair.first, binding)
|
69
|
+
|
70
|
+
"#{str}\n#{set_local_var(pair.first, pair.last)}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_local_vars(binding, in_str)
|
75
|
+
binding.local_variables.inject(in_str) do |str, var|
|
76
|
+
next str if var_excluded?(var, binding)
|
77
|
+
|
78
|
+
"#{str}\n#{set_local_var(var, binding.local_variable_get(var))}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_instance_vars(binding, in_str)
|
83
|
+
binding.eval('instance_variables').inject(in_str) do |str, var|
|
84
|
+
next str if var_excluded?(var, binding)
|
85
|
+
|
86
|
+
"#{str}\n#{set_local_var(var, binding.eval("instance_variable_get('#{var}')"))}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_local_var(name, object)
|
91
|
+
serialized = object.opal_serialize
|
92
|
+
if serialized
|
93
|
+
"#{name} = #{serialized}"
|
94
|
+
else
|
95
|
+
"self.class.define_method(:#{name}) "\
|
96
|
+
"{ raise 'Attempt to access the variable #{name} "\
|
97
|
+
'that was defined in the spec, but its value could not be serialized '\
|
98
|
+
"so it is undefined on the client.' }"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Opal
|
2
|
+
# strips off stuff that confuses things when transmitting to the client
|
3
|
+
# and prints offending code if it can't be compiled
|
4
|
+
def self.hyperspec_compile(str, opts = {})
|
5
|
+
compile(str, opts).gsub("// Prepare super implicit arguments\n", '')
|
6
|
+
.delete("\n").gsub('(Opal);', '(Opal)')
|
7
|
+
# rubocop:disable Lint/RescueException
|
8
|
+
# we are going to reraise it anyway, so its fine to catch EVERYTHING!
|
9
|
+
rescue Exception => e
|
10
|
+
puts "puts could not compile: \n\n#{str}\n\n"
|
11
|
+
raise e
|
12
|
+
end
|
13
|
+
# rubocop:enable Lint/RescueException
|
14
|
+
end
|
15
|
+
|
16
|
+
module Unparser
|
17
|
+
class Emitter
|
18
|
+
# Emitter for send
|
19
|
+
class Send < self
|
20
|
+
def local_variable_clash?
|
21
|
+
selector =~ /^[A-Z]/ ||
|
22
|
+
local_variable_scope.local_variable_defined_for_node?(node, selector)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module MethodSource
|
29
|
+
class << self
|
30
|
+
alias original_lines_for_before_hyper_spec lines_for
|
31
|
+
alias original_source_helper_before_hyper_spec source_helper
|
32
|
+
|
33
|
+
def source_helper(source_location, name = nil)
|
34
|
+
source_location[1] = 1 if source_location[0] == '(pry)'
|
35
|
+
original_source_helper_before_hyper_spec source_location, name
|
36
|
+
end
|
37
|
+
|
38
|
+
def lines_for(file_name, name = nil)
|
39
|
+
if file_name == '(pry)'
|
40
|
+
HyperSpec.current_pry_code_block
|
41
|
+
else
|
42
|
+
original_lines_for_before_hyper_spec file_name, name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Object
|
49
|
+
def opal_serialize
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Hash
|
55
|
+
def opal_serialize
|
56
|
+
"{#{collect { |k, v| "#{k.opal_serialize} => #{v.opal_serialize}" }.join(', ')}}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Array
|
61
|
+
def opal_serialize
|
62
|
+
"[#{collect { |v| v.opal_serialize }.join(', ')}]"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
[FalseClass, Float, Integer, NilClass, String, Symbol, TrueClass].each do |klass|
|
67
|
+
klass.send(:define_method, :opal_serialize) do
|
68
|
+
inspect
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# rubocop:disable Lint/UnifiedInteger - patch for ruby prior to 2.4
|
73
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0')
|
74
|
+
[Bignum, Fixnum].each do |klass|
|
75
|
+
klass.send(:define_method, :opal_serialize) do
|
76
|
+
inspect
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# rubocop:enable Lint/UnifiedInteger
|
81
|
+
|
82
|
+
class Time
|
83
|
+
def to_opal_expression
|
84
|
+
"Time.parse('#{inspect}')"
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module RailsControllerHelpers
|
4
|
+
def self.included(base)
|
5
|
+
base.include ControllerHelpers
|
6
|
+
base.include Helpers
|
7
|
+
routes = ::Rails.application.routes
|
8
|
+
routes.disable_clear_and_finalize = true
|
9
|
+
routes.clear!
|
10
|
+
routes.draw { get "/#{base.route_root}/:id", to: "#{base.route_root}#test" }
|
11
|
+
::Rails.application.routes_reloader.paths.each { |path| load(path) }
|
12
|
+
routes.finalize!
|
13
|
+
ActiveSupport.on_load(:action_controller) { routes.finalize! }
|
14
|
+
ensure
|
15
|
+
routes.disable_clear_and_finalize = false
|
16
|
+
end
|
17
|
+
|
18
|
+
module Helpers
|
19
|
+
def ping!
|
20
|
+
head(:no_content)
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def mount_component!
|
25
|
+
@page << '<%= react_component @component_name, @component_params, '\
|
26
|
+
"{ prerender: #{@render_on != :client_only} } %>"
|
27
|
+
end
|
28
|
+
|
29
|
+
def application!(file)
|
30
|
+
@page << "<%= javascript_include_tag '#{file}' %>"
|
31
|
+
end
|
32
|
+
|
33
|
+
def style_sheet!(file)
|
34
|
+
@page << "<%= stylesheet_link_tag '#{file}' %>"
|
35
|
+
end
|
36
|
+
|
37
|
+
def deliver!
|
38
|
+
@render_params[:inline] = @page
|
39
|
+
response.headers['Cache-Control'] = 'max-age=120'
|
40
|
+
response.headers['X-Tracking-ID'] = '123456'
|
41
|
+
render @render_params
|
42
|
+
end
|
43
|
+
|
44
|
+
def server_only?
|
45
|
+
@render_on == :server_only
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -59,6 +59,10 @@ if RUBY_ENGINE == 'opal'
|
|
59
59
|
ticker
|
60
60
|
end
|
61
61
|
|
62
|
+
def init(scale: 1, resolution: 10)
|
63
|
+
update_lolex(Time.now, scale, resolution)
|
64
|
+
end
|
65
|
+
|
62
66
|
def update_lolex(time, scale, resolution)
|
63
67
|
`#{@lolex}.uninstall()` && return if scale.nil?
|
64
68
|
@mock_start_time = time.to_f * 1000
|
@@ -80,6 +84,14 @@ if RUBY_ENGINE == 'opal'
|
|
80
84
|
end
|
81
85
|
end
|
82
86
|
|
87
|
+
# create an alias for Lolex.init so we can say Timecop.init on the client
|
88
|
+
|
89
|
+
class Timecop
|
90
|
+
def self.init(*args)
|
91
|
+
Lolex.init(*args)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
83
95
|
else
|
84
96
|
require 'timecop'
|
85
97
|
|
@@ -136,7 +148,7 @@ else
|
|
136
148
|
|
137
149
|
def evaluate_ruby(&block)
|
138
150
|
if @capybara_page
|
139
|
-
@capybara_page.
|
151
|
+
@capybara_page.internal_evaluate_ruby(yield)
|
140
152
|
else
|
141
153
|
pending_evaluations << block
|
142
154
|
end
|
@@ -144,7 +156,7 @@ else
|
|
144
156
|
|
145
157
|
def run_pending_evaluations
|
146
158
|
return if pending_evaluations.empty?
|
147
|
-
@capybara_page.
|
159
|
+
@capybara_page.internal_evaluate_ruby(pending_evaluations.collect(&:call).join("\n"))
|
148
160
|
@pending_evaluations ||= []
|
149
161
|
end
|
150
162
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module HyperSpec
|
2
|
+
module Internal
|
3
|
+
module WindowSizing
|
4
|
+
private
|
5
|
+
|
6
|
+
STD_SIZES = {
|
7
|
+
small: [480, 320],
|
8
|
+
mobile: [640, 480],
|
9
|
+
tablet: [960, 640],
|
10
|
+
large: [1920, 6000],
|
11
|
+
default: [1024, 768]
|
12
|
+
}
|
13
|
+
|
14
|
+
def determine_size(width, height)
|
15
|
+
width, height = [height, width] if width == :portrait
|
16
|
+
width, height = width if width.is_a? Array
|
17
|
+
portrait = true if height == :portrait
|
18
|
+
width ||= :default
|
19
|
+
width, height = STD_SIZES[width] if STD_SIZES[width]
|
20
|
+
width, height = [height, width] if portrait
|
21
|
+
[width + debugger_width, height]
|
22
|
+
end
|
23
|
+
|
24
|
+
def debugger_width
|
25
|
+
RSpec.configuration.debugger_width ||= begin
|
26
|
+
hs_internal_resize_to(1000, 500) do
|
27
|
+
sleep RSpec.configuration.wait_for_initialization_time
|
28
|
+
end
|
29
|
+
inner_width = evaluate_script('window.innerWidth')
|
30
|
+
1000 - inner_width
|
31
|
+
end
|
32
|
+
RSpec.configuration.debugger_width
|
33
|
+
end
|
34
|
+
|
35
|
+
def hs_internal_resize_to(width, height)
|
36
|
+
Capybara.current_session.current_window.resize_to(width, height)
|
37
|
+
yield if block_given?
|
38
|
+
wait_for_size(width, height)
|
39
|
+
end
|
40
|
+
|
41
|
+
def wait_for_size(width, height)
|
42
|
+
@start_time = Capybara::Helpers.monotonic_time
|
43
|
+
@stable_count_w = @stable_count_h = 0
|
44
|
+
prev_size = [0, 0]
|
45
|
+
loop do
|
46
|
+
sleep 0.05
|
47
|
+
curr_size = evaluate_script('[window.innerWidth, window.innerHeight]')
|
48
|
+
|
49
|
+
return true if curr_size == [width, height] || stalled?(prev_size, curr_size)
|
50
|
+
|
51
|
+
prev_size = curr_size
|
52
|
+
check_time!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def check_time!
|
57
|
+
if (Capybara::Helpers.monotonic_time - @start_time) >
|
58
|
+
Capybara.current_session.config.default_max_wait_time
|
59
|
+
raise Capybara::WindowError,
|
60
|
+
'Window size not stable within '\
|
61
|
+
"#{Capybara.current_session.config.default_max_wait_time} seconds."
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def stalled?(prev_size, curr_size)
|
66
|
+
# some maximum or minimum is reached and size doesn't change anymore
|
67
|
+
@stable_count_w += 1 if prev_size[0] == curr_size[0]
|
68
|
+
@stable_count_h += 1 if prev_size[1] == curr_size[1]
|
69
|
+
@stable_count_w > 4 || @stable_count_h > 4
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'hyper-spec'
|
2
|
+
|
3
|
+
class HyperSpecTestController < SimpleDelegator
|
4
|
+
include HyperSpec::ControllerHelpers
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :sprocket_server
|
8
|
+
attr_reader :asset_path
|
9
|
+
|
10
|
+
def wrap(app:, append_path: 'app', asset_path: '/assets')
|
11
|
+
@sprocket_server = Opal::Sprockets::Server.new do |s|
|
12
|
+
s.append_path append_path
|
13
|
+
end
|
14
|
+
|
15
|
+
@asset_path = asset_path
|
16
|
+
|
17
|
+
::Rack::Builder.app(app) do
|
18
|
+
map "/#{HyperSpecTestController.route_root}" do
|
19
|
+
use HyperSpecTestController
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def sprocket_server
|
26
|
+
self.class.sprocket_server
|
27
|
+
end
|
28
|
+
|
29
|
+
def asset_path
|
30
|
+
self.class.asset_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def ping!
|
34
|
+
[204, {}, []]
|
35
|
+
end
|
36
|
+
|
37
|
+
def application!(file)
|
38
|
+
@page << Opal::Sprockets.javascript_include_tag(
|
39
|
+
file,
|
40
|
+
debug: true,
|
41
|
+
sprockets: sprocket_server.sprockets,
|
42
|
+
prefix: asset_path
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def json!
|
47
|
+
@page << Opal::Sprockets.javascript_include_tag(
|
48
|
+
'json',
|
49
|
+
debug: true,
|
50
|
+
sprockets: sprocket_server.sprockets,
|
51
|
+
prefix: asset_path
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def style_sheet!(_file_); end
|
57
|
+
|
58
|
+
def deliver!
|
59
|
+
[200, { 'Content-Type' => 'text/html' }, [@page]]
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(env)
|
63
|
+
__setobj__(Rack::Request.new(env))
|
64
|
+
params[:id] = path.split('/').last
|
65
|
+
test
|
66
|
+
end
|
67
|
+
end
|