Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@wied03
Created April 29, 2016 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wied03/a6b198a6d8820e8328037183e7b5c6f1 to your computer and use it in GitHub Desktop.
Save wied03/a6b198a6d8820e8328037183e7b5c6f1 to your computer and use it in GitHub Desktop.
React tests with RSpec
require 'components/base'
require 'react-opal'
require 'components/mixins/default'
class Components::Base::Option
include React::Component
include Components::Mixins::Default
def option_args
result = { value: params[:value] }
with_non_nil_attributes! result, :disabled
end
def render
option option_args do
params[:children]
end
end
end
require 'spec_helper'
require 'components/base/option'
describe Components::Base::Option do
include_context :react_component_testing
describe '#render' do
context 'enabled' do
let(:react_arguments) { { value: '22', children: 'whatup' } }
it { is_expected.to render_markup '<option value="22">whatup</option>' }
end
context 'disabled' do
let(:react_arguments) { { value: '22', children: 'whatup', disabled: true } }
it { is_expected.to render_markup '<option value="22" disabled="">whatup</option>' }
end
end
end
# :reek:BooleanParameter and :reek:ControlParameter and :reek:DataClump
module ReactDomHelpers
def render_element(react_element)
`React.addons.TestUtils.renderIntoDocument(#{react_element})`
end
def get_markup_without_id(rendered_element = subject)
dom_node = React.find_dom_node rendered_element
html_with_id = `#{dom_node}.outerHTML`
html_with_id.gsub /\sdata-reactid=".*?"/, ''
end
def get_dom_node(rendered_element)
React.find_dom_node rendered_element
end
def get_jq_node(rendered_element)
dom_node = get_dom_node rendered_element
dom_node ? Element.find(dom_node) : nil
end
# TODO: Improve this
# rubocop:disable Metrics/PerceivedComplexity
def find_element_jq_nodes(rendered_element, element_type, element_id = nil, element_name = nil, use_self = false)
jq_dom_node = get_jq_node rendered_element
return nil unless jq_dom_node
if use_self
matching = jq_dom_node.tag_name == element_type
matching &&= if element_id
jq_dom_node.id == element_id
else
jq_dom_node[:name] == element_name
end
return matching ? jq_dom_node : nil
end
search_clause = if element_id
"#{element_type}[id=#{element_id}]"
elsif element_name
"#{element_type}[name='#{element_name}']"
else
element_type
end
elements = jq_dom_node.find(search_clause)
elements.any? ? elements : nil
end
# rubocop:enable Metrics/PerceivedComplexity
def get_unresolved_promise(rendered_element)
# if we're operating on this from the before block, we may not have a resolved subject yet
if rendered_element.is_a? Promise
rendered_element.resolved? ? Promise.value(rendered_element.value) : rendered_element
else
Promise.value rendered_element
end
end
def simulate_click(jq_node, descriptor)
info_prefix = "Button #{descriptor} click process"
puts "#{info_prefix} Triggering change event with React" if output_debug_messages
native_node = jq_node.get[0]
`React.addons.TestUtils.Simulate.click(#{native_node})`
puts "#{info_prefix} Trigger complete" if output_debug_messages
end
def click_button(element_id: nil,
button_text: nil,
use_self: false)
promise = get_unresolved_promise rendered_element
# With Opal, named arguments won't be found inside the block, so re-declare them here
# rubocop:disable Style/ParallelAssignment
element_id, button_text, use_self = [element_id, button_text, use_self]
# rubocop:enable Style/ParallelAssignment
promise.then do |rendered_element|
button_nodes = find_element_jq_nodes(rendered_element, :button, element_id, nil, use_self)
# If no buttons exist, we'll get nil, if buttons do exist, we need a Ruby array before we can use find
jq_node = (button_nodes || []).to_a.find do |element|
if element_id
element[:id] == element_id
else
element.text == button_text
end
end
descriptor = (element_id || button_text)
raise_error = lambda do |msg|
markup = `#{get_jq_node(rendered_element).get[0]}.outerHTML`
raise "Cannot click button #{descriptor} because #{msg}, current markup is: #{markup}"
end
raise_error["no 'button' node exists under #{react_element}"] unless jq_node
is_disabled = lambda do |node|
node.has_attribute? 'disabled'
end
raise_error['the button is disabled'] if is_disabled[jq_node]
simulate_click jq_node, descriptor
promise
end
end
def simulate_change(jq_node, new_value, descriptor)
info_prefix = "Input #{descriptor} value change from #{jq_node.value} to #{new_value} -"
puts "#{info_prefix} Changing DOM value" if output_debug_messages
jq_node.value = new_value
puts "#{info_prefix} Triggering change event with React" if output_debug_messages
native_node = jq_node.get[0]
`React.addons.TestUtils.Simulate.change(#{native_node}, {target: {value: #{new_value}}})`
puts "#{info_prefix} Trigger complete" if output_debug_messages
end
def enter_text_value(new_value:,
element_id: nil,
element_name: nil,
use_self: false)
promise = get_unresolved_promise rendered_element
# With Opal, named arguments won't be found inside the block, so re-declare them here
# rubocop:disable Style/ParallelAssignment
new_value, element_id, use_self = [new_value, element_id, use_self]
# rubocop:enable Style/ParallelAssignment
promise.then do |rendered_element|
jq_node = find_element_jq_nodes rendered_element, :input, element_id, element_name, use_self = use_self
descriptor = (element_id || element_name)
# Browser always does strings
new_value = new_value.to_s
raise_error = lambda do |msg|
markup = `#{get_jq_node(rendered_element).get[0]}.outerHTML`
raise "Cannot change input #{descriptor} to #{new_value} because #{msg}, current markup is: #{markup}"
end
raise_error["no 'input' node exists under #{react_element}"] unless jq_node
is_disabled = lambda do |node|
node.has_attribute? 'disabled'
end
raise_error['the input text box is disabled'] if is_disabled[jq_node]
simulate_change jq_node, new_value, descriptor
promise
end
end
# event_promise - :update to wait for on update, :mount to wait for :mount
def choose_select_option(new_value:,
element_id: nil,
element_name: nil)
# With Opal, named arguments won't be found inside the block, so re-declare them here
# rubocop:disable Style/ParallelAssignment
new_value, element_id = [new_value, element_id]
# rubocop:enable Style/ParallelAssignment
stable_component_promise.then do |rendered_element|
jq_node = find_element_jq_nodes rendered_element, :select, element_id, element_name
descriptor = (element_id || element_name)
# Browser always does strings
new_value = new_value.to_s
raise_error = lambda do |msg|
markup = `#{get_jq_node(rendered_element).get[0]}.outerHTML`
raise "Cannot change select #{descriptor} to #{new_value} because #{msg}, current markup is: #{markup}"
end
raise_error["no 'select' node exists under #{react_element}"] unless jq_node
is_disabled = lambda do |node|
node.has_attribute? 'disabled'
end
raise_error['the select is disabled'] if is_disabled[jq_node]
option_element = jq_node.find "option[value=#{new_value}]"
raise_error["no option was found, possible values are #{jq_node.find('option').map(&:value)}"] unless option_element.any?
raise_error['the option is disabled'] if is_disabled[option_element]
simulate_change jq_node, new_value, descriptor
end
end
end
require 'grand_central'
RSpec.shared_context :react_component_testing do
include ReactDomHelpers
let(:output_debug_messages) { false }
let(:react_arguments) { [] }
let(:react_component_class) { described_class }
let(:internal_component_class) do
contexts_local = contexts
klass = react_component_class
contexts_local = contexts_local.merge dispatch: ->(action) { store_mock.dispatch(action) }
Class.new do
include React::Component
contexts_local.each do |key, value|
provide_context(key, value.class) do
value
end
end
define_method(:render) do
present klass,
params
end
end
end
let(:html_form_name) do
contexts && contexts[:html_form]
end
before do
# Avoid the previous state (this fixed the test colluding)
React::ComponentFactory.clear_component_class_cache
# Avoid returning anything
nil
end
after do
if rendered_element
dom_node = get_dom_node rendered_element
# Had problems when didn't try and unmount from the parent node
dom_node = `#{dom_node}.parentNode`
unless React.unmount_component_at_node dom_node
puts "WARNING: Unable to unmount component at node #{`#{dom_node}.outerHTML`}"
end
end
end
let(:react_element) do
arguments = if react_arguments.is_a? Hash
react_arguments
else
with_only_non_default_options(*react_arguments)
end
# will always be hash, so took out splat
React.create_element internal_component_class, arguments
end
let(:rendered_element) do
render_element react_element
end
def stable_component_promise
# for some reason, if we don't "yield", our test results do not report as they execute, just at the end
delay_with_promise 0 do
# Force an initial render if not already, memoization will make this a noop if we already have
rendered_element
end
end
subject { stable_component_promise }
# For debugging
let(:rendered_markup) do
Native(get_dom_node(rendered_element)).outerHTML
end
let(:contexts) do
{}
end
def class_with_dummy_state(klass = described_class)
Class.new(klass) do
define_state(:dummy) { nil }
end
end
def trigger_dummy_state_from_event
# Trigger an update so that our choose_select in before has an update promise value to wait on
# since this is called from a React event handler, 'this' will be the React component the event came from
# rendered_element may not be the same element if we are testing with context, etc.
`this.dummy = 'foo'`
end
def self.error_handling_subject
let(:rendered_element) { nil }
subject do
lambda do
render_element react_element
end
end
end
def with_only_non_default_options(*options)
have_values = options.select do |option|
respond_to? option
end
as_array = have_values.map do |arg|
value = send arg
[arg, value]
end
Hash[as_array]
end
let(:store_mock) { double(GrandCentral::Store) }
let(:actions_received) { [] }
before do
allow(store_mock).to receive(:dispatch) do |action|
actions_received << action
end
end
end
@erlandsona
Copy link

@wied03, what's in your spec_helper? I also had to patch opal-rspec-rails to enable the .opal file extension for specs...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment