require 'spec_helper' if opal? describe React::Component, type: :component do after(:each) do React::API.clear_component_class_cache end it 'defines component spec methods' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def initialize(native = nil) end def render React.create_element('div') end end # Class Methods expect(Foo).to respond_to('initial_state') expect(Foo).to respond_to('default_props') expect(Foo).to respond_to('prop_types') # Instance method expect(Foo.new).to respond_to('component_will_mount') expect(Foo.new).to respond_to('component_did_mount') expect(Foo.new).to respond_to('component_will_receive_props') expect(Foo.new).to respond_to('should_component_update?') expect(Foo.new).to respond_to('component_will_update') expect(Foo.new).to respond_to('component_did_update') expect(Foo.new).to respond_to('component_will_unmount') end describe 'Life Cycle' do let(:element_to_render) { React.create_element(Foo) } before(:each) do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render React.create_element('div') { 'lorem' } end end end it 'invokes `before_mount` registered methods when `componentWillMount()`' do Foo.class_eval do before_mount :bar, :bar2 def bar; end def bar2; end end expect_any_instance_of(Foo).to receive(:bar) expect_any_instance_of(Foo).to receive(:bar2) React::Test::Utils.render_into_document(element_to_render) end it 'invokes `after_mount` registered methods when `componentDidMount()`' do Foo.class_eval do after_mount :bar3, :bar4 def bar3; end def bar4; end end expect_any_instance_of(Foo).to receive(:bar3) expect_any_instance_of(Foo).to receive(:bar4) React::Test::Utils.render_into_document(element_to_render) end it 'allows multiple class declared life cycle hooker' do stub_const 'FooBar', Class.new Foo.class_eval do before_mount :bar def bar; end end FooBar.class_eval do include React::Component after_mount :bar2 def bar2; end def render React.create_element('div') { 'lorem' } end end expect_any_instance_of(Foo).to receive(:bar) React::Test::Utils.render_into_document(element_to_render) end it 'allows block for life cycle callback' do Foo.class_eval do before_mount do set_state({ foo: "bar" }) end end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state[:foo]).to be('bar') end end describe 'New style setter & getter' do before(:each) do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render div { state.foo } end end end it 'implicitly will create a state variable when first written' do Foo.class_eval do before_mount do state.foo! 'bar' end end expect(Foo).to render_static_html('
bar
') end it 'allows kernal method names like "format" to be used as state variable names' do Foo.class_eval do before_mount do state.format! 'yes' state.foo! state.format end end expect(Foo).to render_static_html('
yes
') end it 'returns an observer with the bang method and no arguments' do Foo.class_eval do before_mount do state.foo!(state.baz!.class.name) end end expect(Foo).to render_static_html('
React::Observable
') end it 'returns the current value of a state when written' do Foo.class_eval do before_mount do state.baz! 'bar' state.foo!(state.baz!('pow')) end end expect(Foo).to render_static_html('
bar
') end it 'can access an explicitly defined state`' do Foo.class_eval do define_state foo: :bar end expect(Foo).to render_static_html('
bar
') end end describe 'State setter & getter' do let(:element_to_render) { React.create_element(Foo) } before(:each) do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render React.create_element('div') { 'lorem' } end end end it 'defines setter using `define_state`' do Foo.class_eval do define_state :foo before_mount :set_up def set_up mutate.foo 'bar' end end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be('bar') end it 'defines init state by passing a block to `define_state`' do Foo.class_eval do define_state(:foo) { 10 } end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be(10) end it 'defines getter using `define_state`' do Foo.class_eval do define_state(:foo) { 10 } before_mount :bump def bump mutate.foo(state.foo + 20) end end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be(30) end it 'defines multiple state accessors by passing array to `define_state`' do Foo.class_eval do define_state :foo, :foo2 before_mount :set_up def set_up mutate.foo 10 mutate.foo2 20 end end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be(10) expect(instance.state.foo2).to be(20) end it 'invokes `define_state` multiple times to define states' do Foo.class_eval do define_state(:foo) { 30 } define_state(:foo2) { 40 } end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be(30) expect(instance.state.foo2).to be(40) end it 'can initialize multiple state variables with a block' do Foo.class_eval do define_state(:foo, :foo2) { 30 } end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.foo).to be(30) expect(instance.state.foo2).to be(30) end it 'can mix multiple state variables with initializers and a block' do Foo.class_eval do define_state(:x, :y, foo: 1, bar: 2) {3} end instance = React::Test::Utils.render_into_document(element_to_render) expect(instance.state.x).to be(3) expect(instance.state.y).to be(3) expect(instance.state.foo).to be(1) expect(instance.state.bar).to be(2) end it 'gets state in render method' do Foo.class_eval do define_state(:foo) { 10 } def render React.create_element('div') { state.foo } end end instance = React::Test::Utils.render_into_document(element_to_render) expect(`#{instance.dom_node}.textContent`).to eq('10') end it 'supports original `setState` as `set_state` method' do Foo.class_eval do before_mount do self.set_state(foo: 'bar') end end instance = renderToDocument(Foo) expect(instance.state[:foo]).to be('bar') end it 'supports original `replaceState` as `set_state!` method' do Foo.class_eval do before_mount do set_state(foo: 'bar') set_state!(bar: 'lorem') end end element = renderToDocument(Foo) puts "*************************************************************************************" puts "element.state[:foo] = #{element.state[:foo]}" puts "element.state[:bar] = #{element.state[:bar]}" puts "*************************************************************************************" expect(element.state[:foo]).to be_nil expect(element.state[:bar]).to eq('lorem') end it 'supports original `state` method' do Foo.class_eval do before_mount do self.set_state(foo: 'bar') end def render div { self.state[:foo] } end end expect(Foo).to render_static_html('
bar
') end it 'transforms state getter to Ruby object' do Foo.class_eval do define_state :foo before_mount do mutate.foo [{a: "Hello"}] end def render div { state.foo[0][:a] } end end expect(Foo).to render_static_html('
Hello
') end end describe 'Props' do describe 'this.props could be accessed through `params` method' do before do stub_const 'Foo', Class.new Foo.class_eval do include React::Component end end it 'reads from parent passed properties through `params`' do Foo.class_eval do def render React.create_element('div') { params[:prop] } end end element = renderToDocument(Foo, prop: 'foobar') expect(`#{element.dom_node}.textContent`).to eq('foobar') end it 'accesses nested params as orignal Ruby object' do Foo.class_eval do def render React.create_element('div') { params[:prop][0][:foo] } end end element = renderToDocument(Foo, prop: [{foo: 10}]) expect(`#{element.dom_node}.textContent`).to eq('10') end end describe 'Props Updating', v13_only: true do before do stub_const 'Foo', Class.new Foo.class_eval do include React::Component end end it 'supports original `setProps` as method `set_props`' do Foo.class_eval do def render React.create_element('div') { params[:foo] } end end element = renderToDocument(Foo, {foo: 10}) element.set_props(foo: 20) expect(`#{element.dom_node}.innerHTML`).to eq('20') end it 'supports original `replaceProps` as method `set_props!`' do Foo.class_eval do def render React.create_element('div') { params[:foo] ? 'exist' : 'null' } end end instance = renderToDocument(Foo, {foo: 10}) instance.set_props!(bar: 20) expect(`#{instance.dom_node}.innerHTML`).to eq('null') end end describe 'Prop validation' do before do stub_const 'Foo', Class.new Foo.class_eval do include React::Component end end it 'specifies validation rules using `params` class method' do Foo.class_eval do params do requires :foo, type: String optional :bar end end expect(Foo.prop_types).to have_key(:_componentValidator) end it 'logs error in warning if validation failed' do stub_const 'Lorem', Class.new Foo.class_eval do params do requires :foo requires :lorem, type: Lorem optional :bar, type: String end def render; div; end end %x{ var log = []; var org_warn_console = window.console.warn; var org_error_console = window.console.error; window.console.warn = window.console.error = function(str){log.push(str)} } renderToDocument(Foo, bar: 10, lorem: Lorem.new) `window.console.warn = org_warn_console; window.console.error = org_error_console;` expect(`log[0]`).to match(/Warning: Failed prop( type|Type): In component `Foo`\nRequired prop `foo` was not specified\nProvided prop `bar` could not be converted to String/) end it 'should not log anything if validation pass' do stub_const 'Lorem', Class.new Foo.class_eval do params do requires :foo requires :lorem, type: Lorem optional :bar, type: String end def render; div; end end %x{ var log = []; var org_warn_console = window.console.warn; var org_error_console = window.console.error window.console.warn = window.console.error = function(str){log.push(str)} } renderToDocument(Foo, foo: 10, bar: '10', lorem: Lorem.new) `window.console.warn = org_warn_console; window.console.error = org_error_console;` expect(`log`).to eq([]) end end describe 'Default props' do it 'sets default props using validation helper' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component params do optional :foo, default: 'foo' optional :bar, default: 'bar' end def render div { params[:foo] + '-' + params[:bar]} end end expect(Foo).to render_static_html('
lorem-bar
').with_params(foo: 'lorem') expect(Foo).to render_static_html('
foo-bar
') end end end describe 'Anonymous Component' do it "will not generate spurious warning messages" do foo = Class.new(React::Component::Base) foo.class_eval do def render; "hello" end end %x{ var log = []; var org_warn_console = window.console.warn; var org_error_console = window.console.error window.console.warn = window.console.error = function(str){log.push(str)} } renderToDocument(foo) `window.console.warn = org_warn_console; window.console.error = org_error_console;` expect(`log`).to eq([]) end end describe 'Render Error Handling' do before(:each) do %x{ window.test_log = []; window.org_warn_console = window.console.warn; window.org_error_console = window.console.error window.console.warn = window.console.error = function(str){window.test_log.push(str)} } end it "will generate a message if render returns something other than an Element or a String" do foo = Class.new(React::Component::Base) foo.class_eval do def render; Hash.new; end end renderToDocument(foo) `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;` expect(`test_log`.first).to match /Instead the Hash \{\} was returned/ end it "will generate a message if render returns a Component class" do stub_const 'Foo', Class.new(React::Component::Base) foo = Class.new(React::Component::Base) foo.class_eval do def render; Foo; end end renderToDocument(foo) `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;` expect(`test_log`.first).to match /Did you mean Foo()/ end it "will generate a message if more than 1 element is generated" do foo = Class.new(React::Component::Base) foo.class_eval do def render; "hello".span; "goodby".span; end end renderToDocument(foo) `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;` expect(`test_log`.first).to match /Instead 2 elements were generated/ end it "will generate a message if the element generated is not the element returned" do foo = Class.new(React::Component::Base) foo.class_eval do def render; "hello".span; "goodby".span.delete; end end renderToDocument(foo) `window.console.warn = window.org_warn_console; window.console.error = window.org_error_console;` expect(`test_log`.first).to match /A different element was returned than was generated within the DSL/ end end describe 'Event handling' do before do stub_const 'Foo', Class.new Foo.class_eval do include React::Component end end it 'works in render method' do Foo.class_eval do define_state(:clicked) { false } def render React.create_element('div').on(:click) do mutate.clicked true end end end element = React.create_element(Foo) instance = React::Test::Utils.render_into_document(element) React::Test::Utils.simulate(:click, instance) expect(instance.state.clicked).to eq(true) end it 'invokes handler on `this.props` using emit' do Foo.class_eval do param :_onFooSubmit, type: Proc after_mount :setup def setup puts "***************************** about to emit******************************" self.emit(:foo_submit, 'bar') puts "***************************** emitted!!! ********************************" rescue Exception => e puts "FAILED FAILED FAILED #{e}" end def render React.create_element('div') end end expect { |b| element = React.create_element(Foo).on(:foo_submit, &b) React::Test::Utils.render_into_document(element) }.to yield_with_args('bar') end it 'invokes handler with multiple params using emit' do Foo.class_eval do param :_onFooInvoked, type: Proc after_mount :setup def setup self.emit(:foo_invoked, [1,2,3], 'bar') end def render React.create_element('div') end end expect { |b| element = React.create_element(Foo).on(:foo_invoked, &b) React::Test::Utils.render_into_document(element) }.to yield_with_args([1,2,3], 'bar') end end describe '#refs' do before do stub_const 'Foo', Class.new Foo.class_eval do include React::Component end end it 'correctly assigns refs' do Foo.class_eval do def render React.create_element('input', type: :text, ref: :field) end end instance = renderToDocument(Foo) expect(instance.refs[:field]).not_to be_nil end it 'accesses refs through `refs` method' do Foo.class_eval do def render React.create_element('input', type: :text, ref: :field).on(:click) do refs[:field].value = 'some_stuff' end end end instance = React::Test::Utils.render_into_document(React.create_element(Foo)) React::Test::Utils.simulate(:click, instance) expect(instance.refs[:field].value).to eq('some_stuff') end it "allows access the actual DOM node", v13_exclude: true do Foo.class_eval do after_mount do dom = refs[:my_div].to_n `dom.innerHTML = 'Modified'` end def render React.create_element('div', ref: :my_div) { "Original Content" } end end instance = renderToDocument(Foo) expect(`#{instance.dom_node}.innerHTML`).to eq('Modified') end end describe '#render' do it 'supports element building helpers' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render div do span { params[:foo] } end end end stub_const 'Bar', Class.new Bar.class_eval do include React::Component def render div do present Foo, foo: 'astring' end end end expect(Bar).to render_static_html('
astring
') end it 'builds single node in top-level render without providing a block' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render div end end expect(Foo).to render_static_html('
') end it 'redefines `p` to make method missing work' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render div { p(class_name: 'foo') p div { 'lorem ipsum' } p(id: '10') } end end markup = '

lorem ipsum

' expect(Foo).to render_static_html(markup) end it 'only overrides `p` in render context' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component before_mount do p 'first' end after_mount do p 'second' end def render div end end expect(Kernel).to receive(:p).with('first') expect(Kernel).to receive(:p).with('second') renderToDocument(Foo) end end describe 'isMounted()' do it 'returns true if after mounted' do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render React.create_element('div') end end component = renderToDocument(Foo) expect(component.mounted?).to eq(true) end end describe '.params_changed?' do before(:each) do stub_const 'Foo', Class.new(React::Component::Base) Foo.define_method :needs_update? do |next_params, next_state| next_params.changed? end @foo = Foo.new(nil) end it "returns false if new and old params are the same" do @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`) expect(@foo.should_component_update?(`{value2: 2, value1: 1}`, `null`)).to be_falsy end it "returns true if new and old params are have different values" do @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`) expect(@foo.should_component_update?(`{value2: 2, value1: 2}`, `null`)).to be_truthy end it "returns true if new and old params are have different keys" do @foo.instance_variable_set("@native", `{props: {value1: 1, value2: 2}}`) expect(@foo.should_component_update?(`{value2: 2, value1: 1, value3: 3}`, `null`)).to be_truthy end end describe '#state_changed?' do empties = [`{}`, `undefined`, `null`, `false`] before(:each) do stub_const 'Foo', Class.new(React::Component::Base) Foo.define_method :needs_update? do |next_params, next_state| next_state.changed? end @foo = Foo.new(nil) end it "returns false if both new and old states are empty" do empties.each do |empty1| empties.each do |empty2| @foo.instance_variable_set("@native", `{state: #{empty1}}`) expect(@foo.should_component_update?(`{}`, empty2)).to be_falsy end end end it "returns true if old state is empty, but new state is not" do empties.each do |empty| @foo.instance_variable_set("@native", `{state: #{empty}}`) expect(@foo.should_component_update?(`{}`, `{foo: 12}`)).to be_truthy end end it "returns true if new state is empty, but old state is not" do empties.each do |empty| @foo.instance_variable_set("@native", `{state: {foo: 12}}`) expect(@foo.should_component_update?(`{}`, empty)).to be_truthy end end it "returns true if new state and old state have different time stamps" do @foo.instance_variable_set("@native", `{state: {'***_state_updated_at-***': 12}}`) expect(@foo.should_component_update?(`{}`, `{'***_state_updated_at-***': 13}`)).to be_truthy end it "returns false if new state and old state have the same time stamps" do @foo.instance_variable_set("@native", `{state: {'***_state_updated_at-***': 12}}`) expect(@foo.should_component_update?(`{}`, `{'***_state_updated_at-***': 12}`)).to be_falsy end end describe '#children' do before(:each) do stub_const 'Foo', Class.new Foo.class_eval do include React::Component def render React.create_element('div') { 'lorem' } end end end it 'returns React::Children collection with child elements' do ele = React.create_element(Foo) { [React.create_element('a'), React.create_element('li')] } instance = React::Test::Utils.render_into_document(ele) children = instance.children expect(children).to be_a(React::Children) expect(children.count).to eq(2) expect(children.map(&:element_type)).to eq(['a', 'li']) end it 'returns an empty Enumerator if there are no children' do ele = React.create_element(Foo) instance = React::Test::Utils.render_into_document(ele) nodes = instance.children.each expect(nodes.size).to eq(0) expect(nodes.count).to eq(0) end end end end