Methods and Features
HyperSpec Methods and Features
Expectation Helpers
These can be used any where within your specs:
on_client- executes code on the clientisomorphic- executes code on the client and the servermount- mounts a hyperstack component in an empty windowbefore_mount- specifies a block of code to be executed before the first call tomount,isomorphicoron_clientinsert_html- insert some html into a pageclient_options- allows options to be specified globallyrun_on_client- same ason_clientbut no value is returnedreload_page- resets the page environmentadd_class- adds a CSS classsize_window- specifies how big the client window should beattributes_on_client- returns any ActiveModel attributes loaded on the client
These methods are used after mounting a component to retrieve events sent outwards from the component:
Expectation Targets
These can be used within expectations replacing the to and not_to methods. The expectation expression must be inclosed in a block.
on_client_to,to_on_client_not- the expression will be evaluated on the client, and matched on the server.
These methods have the following aliases to make your specs more readable:
in addition
with- can be chained with the above methods to pass data to initialize local variables on the client
Other Debugging Aids
The following methods are used primarly at a debug break point, most require you use binding.pry as your debugger:
to_js- returns the ruby code compiled to JS.c?- alias foron_client.ppr- print the results of the ruby expression on the client console.debugger- Sets a debug breakpoint on code running on the client.open_in_chrome- Opens a chrome browser that will load the current state.pause- Halts execution on the server without blocking I/O.
Available Webdrivers
HyperSpec comes integrated with Chrome and Chrome headless webdrivers. The default configuration will run using Chrome headless. To see what is going on set the DRIVER environment variable to chrome
DRIVER=chrome bundle exec rspecTimecop Integration
You can use the timecop gem to control the flow of time within your specs. Hyperspec will coordinate things with the client so the time on the client is kept in sync with the time on the server. So for example if you use Timecop to advance time 1 day on the server, time on the browser will also advance by one day.
See the Client Initialization Options section for how to control the client time zone, and clock resolution.
The no_reset flag
no_reset flagBy default the client environment will be reinitialized at the beginning of every spec. If this is not needed you can speed things up by adding the no_reset flag to a block of specs.
Known Issues
See the last section below for known issues.
Details
The on_client method
on_client methodThe on_client method takes a block. The ruby code inside the block will be executed on the client, and the result will be returned.
it 'will print a message on the client' do
on_client do
puts 'hey I am running here on the client!'
end
endIf the block returns a promise Hyperspec will wait for the promise to be resolved (or rejected) before returning. For example:
it 'waits for a promise' do
start_time = Time.now
result = on_client do
promise = Promise.new
after(10.seconds) { promise.resolve('done!') }
promise
end
expect(result).to eq('done!')
expect(Time.now-start_time).to be >= 10.seconds
endHyperSpec will do its best to reconstruct the result back on the server in some sensible way. Occasionally it just doesn't work, in which case you can end the block with a
nilor some other simple expression, or use therun_on_clientmethod, which does not return the result.
Accessing variables on the client
It is often useful to pass variables from the spec to the client. Hyperspec will copy all your local variables, memoized variables, and instance variables known at the time the on_client block is compiled to the client.</br>
let!(memoized) { 'a memoized variable' }
it 'will pass variables to the client' do
local = 'a local variable'
@instance = 'an instance variable'
result = on_client { [memoized, local, @instance] }
expect(result).to eq [memoized, local, @instance]
endNote that memoized variables are not initialized until first accessed, so you probably want to use the let! method unless you are sure you are accessing the memoized value before sending it to the client.
The value of instance variables initialized on the client are preserved across blocks executed on the client. For example:
it 'remembers instance variables' do
on_client { @total = 0 }
10.times do |i|
# note how we are passing i in
on_client { @total += i }
end
result = on_client { @total }
expect(result).to eq(10 * 11 / 2)
endBe especially careful of this when using the
no_resetflag as instance variables will retain their values between each spec in this mode.
White and Black Listing Variables
By default all local variables, memoized variables, and instance variables in scope in the spec will be copied to the client. This can be controlled through the include_vars and exclude_vars client options.
include_vars can be set to
an array of symbols: only those vars will be copied,
a single symbol: only that var will be copied,
any other truthy value: all vars will be copied (the default)
or nil, false, or an empty array: no vars will be copied.
exclude_vars can be set to
an array of symbols - those vars will not be copied,
a single symbol - only that var will be excluded,
any other truthy value - no vars will be copied,
or nil, false, or an empty array - all vars will be copied (the default).
Examples:
# don't copy vars at all.
client_option exclude_vars: true
# only copy var1 and the instance var @var2
client_option include_vars: [:var1, :@var2]
# only exclude foo_var
client_option exclude_vars: :foo_varNote that the exclude_vars list will take precedence over the include_vars list.
The exclude/include lists can be overridden on an individual call to on_client by providing a hash of names and values to on_client:
result = on_client(var: 12) { var * var }
expect(result).to eq(144)You can do the same thing on expectations using the with method - See Client Expectation Targets.
The isomorphic method
isomorphic methodThe isomorphic method works the same as on_client but in addition it also executes the same block on the server. It is especially useful when doing some testing of ActiveRecord models, where you might want to modify the behavior of the model on server and the client.
it 'can run code the same everywhere!' do
isomorphic do
def factorial(x)
x.zero? ? 1 : x * factorial(x - 1)
end
end
on_the_client = on_client { factorial(7) }
on_the_server = factorial(7)
expect(on_the_client).to eq(on_the_server)
endClient Initialization Options
The first time a spec runs code on the client, it has to initialize a browser context. You can use the client_options (aka client_option) method to specify the following options when the page is loaded.
time_zone- browsers always run in the local time zone, if you want to force the browser to act as if its in a different zone, you can use the time_zone option, and provide any valid zone that the railsin_time_zonemethod will accept.Example:
client_option time_zone: 'Hawaii'clock_resolution: Indicates the resolution that the simulated clock will run at on the client, when using the TimeCop gem. The default value is 20 (milliseconds).include_vars: white list of all vars to be copied to the client. See Accessing Variables on the Client for details.exclude_vars: black list of all vars not to be copied to the client. See Accessing Variables on the Client for details.render_on::client_only(default),:server_only, or:bothHyperstack components can be prerendered on the server. The
render_onoption controls this feature. For exampleserver_onlyis useful to insure components are properly prerendered. See themountmethod below for more details on rendering componentsno_wait: After the page is loaded the system will by default wait until all javascript requests to the server complete before proceeding. Specifyingno_wait: truewill skip this.javascript: The javascript asset to load when mounting the component. By default it will beapplication(.js is assumed). Note that the standard Hyperstack configuration will compile all the client side Ruby assets as well as javascript packages into theapplication.jsfile, so the default will work fine.style_sheet: The style sheet asset to load when mounting the component. By default it will beapplication(.css is assumed).controller- (expert zone!) specify a controller that will be used to mount thecomponent. By default hyper-spec will build a controller and route to handle the request from the client to mount the component.
Any other options not listed above will be passed along to the Rail's controller render method. So for example you could specify some other specific layout using client_option layout: 'special_layout'
Note that this method can be used in the before(:each) block of a spec context to provide options for all the specs in the block.
Mounting Components
The mount method is used to render a component on a page:
it 'can display a component for me' do
mount 'SayHello', name: 'Lannar' do
class SayHello < HyperComponent
param :name
render(DIV) do
"Hello #{name}!"
end
end
end
expect(page).to have_content('Hello Lannar')
endThe mount method has a few options. In it's simplest form you specify just the name of the component that is already defined in your hyperstack code and it will be mounted.
You can add parameters that will be passed to the component as in the above example. As the above example also shows you can also define code within the block. This is just shorthand for defining the code before hand using on_client. The code does not have to be the component being mounted, but might be just some logic to help with the test.
In addition mount can take any of the options provided to client_options (see above.) To provide these options, you must provide a (possibly) empty params hash. For example:
mount 'MyComponent', {... params ... }, {... opts ... }Retrieving Event Data From the Mounted Component
Components receive parameters, and may send callbacks and events back out. To test if a component has sent the appropriate data you can use the following methods:
callback_history_forlast_callback_forclear_callback_history_forevent_history_forlast_event_forclear_event_history_for
it 'can check on a clients events and callbacks' do
mount 'BigTalker' do
class BigTalker < HyperComponent
fires :i_was_clicked
param :call_me_back, type: Proc
before_mount { @click_counter = 0 }
render(DIV) do
BUTTON { 'click me' }.on(:click) do
@click_counter += 1
i_was_clicked!
call_me_back.call(@click_counter)
end
end
end
end
3.times do
find('button').click
end
# the history is an array, one element for each item in the history
expect(event_history_for(:i_was_clicked).length).to eq(3)
# each item in the array is itself an array of the arguments
expect(last_call_back_for(:call_me_back)).to eq([3])
# clearing the history resets the array to empty
clear_event_history_for(:i_was_clicked)
expect(event_history_for(:i_was_clicked).length).to eq(0)
endNote that you must declare the params as type
Proc, or use thefiresmethod to declare an event for the history mechanism to work.
Other Helpers
before_mount
before_mountSpecifies a block of code to be executed before the first call to mount, isomorphic or on_client. This is primarly useful to add to an rspec before(:each) block containing common client code needed by all the specs in the context.
Unlike
mount,isomorphicandon_client,before_mountdoes not load the client page, but will wait for the first of the other methods to be called.
add_class
add_classAdds a CSS class. The first parameter is the name of the class, and the second is a hash of styles, represented in the React style format.
Example: add_class :some_class, borderStyle: :solid adds a class with style border-style: 'solid'
run_on_client
run_on_clientsame as on_client but no value is returned. Useful when the return value may be too complex to marshall and unmarshall using JSON.
reload_page
reload_pageShorthand for mount with no parameters. Useful if you need to reset the client within a spec.
size_window
size_windowIndicates the size of the browser window. The values can be given either symbolically or as two numbers (width and height). Predefined sizes are:
:small: 480 x 320:mobile640 x 480:tablet960 x 64,:large1920 x 6000:default1024 x 768
All of the above can be modified by providing the :portrait option as the first or second parameter.
So for example the following are all equivalent:
size_window(:small, :portrait)size_window(:portrait, :small)size_window(320, 480)
attributes_on_client
attributes_on_clientreturns any ActiveModel attributes loaded on the client. HyperModel will normally begin a load cycle as soon as you access the attribute on the client. However it is sometimes useful to see what attributes have already been loaded.
insert_html
insert_htmltakes a string and inserts it into test page when it is mounted. Useful for testing code that is not dependent on Hyper Components. For example an Opal library that adds some jQuery extensions.
Client Expectation Targets
These can be used within expectations replacing the to and not_to methods. The expectation expression must be inclosed in a block.
For example:
it 'has built-in expectation targets' do
expect { RUBY_ENGINE }.on_client_to eq('opal')
endThe above expectation is short for saying:
result = on_client { RUBY_ENGINE }
expect(result).to eq('opal')These methods have the following aliases to make your specs more readable:
to_on_clienton_client_to_noton_client_not_toto_not_on_clientnot_to_on_clientto_thenthen_to_notthen_not_toto_not_thennot_to_then
The then variants are useful to note that the spec involves a promise, but it does no explicit checking that the result comes from a promise.
In addition the with method can be chained with the above methods to pass data to initialize local variables on the client:
it 'can pass values to the client using the with method' do
expect { foo * foo }.with(foo: 12).to_on_client eq(144)
endBy default HyperSpec will copy all local variables, memoized variables, and instance variables defined in a spec to the client. The specific variables can also be white listed and black listed. The with method overrides any white or black listed values. So for example if you prefer to use the more explicit with method to pass values to the client, you can add client_option exclude_vars: true in a before(:all) block in your spec helper. See Accessing Variables on the Client for details.
Useful Debug Methods
These methods are primarily designed to help debug code and specs.
c?
c?Shorthand for on_client, useful for entering expressions in the pry console, to investigate the state of the client.
pry:> c? { puts 'hello on the console' } # prints hello on the client
-> nilto_js
to_jsTakes a block like on_client but rather than running the code on the client, simply returns the resulting code. This is useful for debugging obscure problems when the Opal compiler or some feature of Hyperspec is suspected as the issue.
ppr
pprTakes a block like on_client and prints the result on the client console using JS console.log. Equivalent to doing
on_client do
begin
...
end.tap { |r| `console.log(r)` }
endThis is useful when the result cannot be usefully returned to the server, or when the result of interest is better looked at as the raw javascript object.
debugger
debuggerThis psuedo method can be inserted into any code executed on the client. It will cause the code to stop, and enter a javascript read-eval loop, within the debug console.
Unfortunately ATM we do not have the technology to enter a Ruby read-eval loop at an arbitrary point on the client.
Note: due to a bug in the Opal compiler your code should not have
debuggeras the last expression in a method or a block. In this situation add any expression (such as nil) after the debugger statement.def foo ... some code ... debugger # this will fail with a compiler syntax error end
open_in_chrome
open_in_chromeBy default specs are run with headless chrome, so there is no visible browser window. The open_in_chrome method will open a browser window, and load it with the current state.
You can also run specs in a visible chrome window by setting the DRIVER environment variable to chrome. i.e. (DRIVER=chrome bundle exec rspec ...)
pause
pauseThe method is typically not needed assuming you are using a multithreaded server like Puma. If for whatever reason the pry debug session is not multithreaded, and you want to try some kind of experiment on the javascript console, and those experiments make requests to the server, you may not get a response, because all threads are in use.
You can resolve this by using the pause method in the debug session which will put the server debug session into a non-blocking loop. You can then experiment in the JS console, and when done release the pause by executing go() in the javascript debug console.
Known Issues
Using visit and the Application Layout
visit and the Application LayoutCurrently this is not well integrated (see issue 398). If you want to visit a page on the website using visit, the following will not work: Timecop integration, and the insert_html and before_mount methods. You will also have to execute this line in your spec:
page.instance_variable_set("@hyper_spec_mounted", true)Upvote issue 398 if this presents a big problem for you.
Some Complex Expressions Do Not Work
This has been fixed in Parser version 2.7 which works with Opal 1.0 So the issue is only with older versions of Opal.
You may get an error like this when running a spec:
(string):1:20: error: unexpected token tOP_ASGN
(string):1: hash.[]("foo") += 1
(string):1:The problem is the unparser incorrectly generates hash.[]("foo") += 1 instead of hash['foo'] += 1.
The good news its pretty easy to find such expressions and replace them with something like
hash["foo"] = hash["foo"] + 1
Last updated
Was this helpful?