require 'spec_helper' require 'test_components' describe "synchronized scopes", js: true do before(:all) do require 'pusher' require 'pusher-fake' Pusher.app_id = "MY_TEST_ID" Pusher.key = "MY_TEST_KEY" Pusher.secret = "MY_TEST_SECRET" require "pusher-fake/support/base" Hyperloop.configuration do |config| config.transport = :pusher config.channel_prefix = "synchromesh" config.opts = {app_id: Pusher.app_id, key: Pusher.key, secret: Pusher.secret}.merge(PusherFake.configuration.web_options) end end before(:all) do TestModel.class_eval do class << self alias pretest_scope scope def scope(name, *args, &block) opts = _synchromesh_scope_args_check(args) self.class.class_eval do attr_reader "#{name}_count".to_sym end instance_variable_set("@#{name}_count", 0) original_proc = opts[:server] opts[:server] = lambda do |*args| instance_variable_set("@#{name}_count", send("#{name}_count")+1); original_proc.call(*args, &block) end pretest_scope name, opts, &block end end end end before(:each) do # spec_helper resets the policy system after each test so we have to setup # before each test stub_const 'TestApplicationPolicy', Class.new TestApplicationPolicy.class_eval do always_allow_connection regulate_all_broadcasts { |policy| policy.send_all } end size_window(:small, :portrait) end it "can use the all scope" do mount "TestComponent2" do class TestComponent2 < React::Component::Base render do "count = #{TestModel.all.count}" end end end page.should have_content("count = 0") m = FactoryGirl.create(:test_model) page.should have_content("count = 1") m.destroy page.should have_content("count = 0") end it "will be updated only when needed" do isomorphic do TestModel.class_eval do scope :scope1, -> { where(completed: true) } scope :scope2, -> { where(completed: true) } end end mount "TestComponent2" do class TestComponent2 < React::Component::Base before_mount do @render_count = 1 end before_update do @render_count = @render_count + 1 end render(:div) do div { "rendered #{@render_count} times"} div { "scope1 count = #{TestModel.scope1.count}" } div { "scope2 count = #{TestModel.scope2.count}"} if TestModel.scope1.count < 2 TestModel.scope1.each do |model| div { model.test_attribute } end end end end page.should have_content('rendered 2 times') page.should have_content('scope1 count = 0') page.should have_content('scope2 count = 0') TestModel.scope1_count.should eq(2) # once for scope1.count and once for scope1.each { .test_attribute } TestModel.scope2_count.should eq(1) m1 = FactoryGirl.create(:test_model, test_attribute: "model 1", completed: true) page.should have_content('rendered 3 times') page.should have_content('scope1 count = 1') page.should have_content('scope2 count = 1') page.should have_content('model 1') TestModel.scope1_count.should eq(3) TestModel.scope2_count.should eq(2) m2 = FactoryGirl.create(:test_model, test_attribute: "model 2", completed: false) page.should have_content('rendered 4 times') page.should have_content('scope1 count = 1') page.should have_content('scope2 count = 1') page.should have_content('model 1') TestModel.scope1_count.should eq(4) TestModel.scope2_count.should eq(3) FactoryGirl.create(:test_model, test_attribute: "model 3", completed: true) page.should have_content('rendered 5 times') page.should have_content('scope1 count = 2') page.should_not have_content('scope2', wait: 0) page.should have_content('model 1') page.should have_content('model 3') TestModel.scope1_count.should eq(5) TestModel.scope2_count.should eq(4) m2.update_attribute(:completed, true) page.should have_content('rendered 6 times') page.should have_content('scope1 count = 3') page.should have_content('model 1') page.should have_content('model 2') page.should have_content('model 3') TestModel.scope1_count.should eq(6) TestModel.scope2_count.should eq(4) # will not change because nothing is viewing it m2.update_attribute(:completed, false) page.should have_content('rendered 7 times') TestModel.scope1_count.should eq(7) m1.update_attribute(:completed, false) page.should have_content('rendered 9 times') page.should have_content('scope1 count = 1') page.should have_content('scope2 count = 1') page.should have_content('model 3') TestModel.scope1_count.should eq(8) TestModel.scope2_count.should eq(5) end it "can have params" do isomorphic do TestModel.class_eval do scope :with_args, ->(match, match2) { where(test_attribute: match) } end end m1 = FactoryGirl.create(:test_model, test_attribute: "123") mount "TestComponent2", m: m1 do class TestComponent2 < React::Component::Base param :m, type: TestModel before_mount do @render_count = 1 end before_update do @render_count = @render_count + 1 end render(:div) do div { "rendered #{@render_count} times"} div { "with_args('match').count = #{TestModel.with_args(params.m.test_attribute, :foo).count}" } end end end m2 = FactoryGirl.create(:test_model) page.should have_content('.count = 1') m2.update_attribute(:test_attribute, m1.test_attribute) page.should have_content('.count = 2') m1.update_attribute(:test_attribute, '456') page.should have_content('.count = 1') end it "scopes with params can be nested" do isomorphic do TestModel.class_eval do scope :scope1, -> { where(completed: true) } scope :with_args, -> (match) { where(test_attribute: match) } end end mount "TestComponent2" do class TestComponent2 < React::Component::Base render(:div) do div { "scope1.scope2.count = #{TestModel.scope1.with_args(:foo).count}" } end end end page.should have_content('scope1.scope2.count = 0') FactoryGirl.create(:test_model, test_attribute: "foo", completed: true) page.should have_content('scope1.scope2.count = 1') end it 'collections passed from server will not interfere with client associations' do user = FactoryGirl.create(:user) 5.times do FactoryGirl.create(:todo, title: 'active', created_by_id: user.id) end isomorphic do Todo.class_eval do scope :active, -> { where('title LIKE ?', 'active') } end end todos = user.authored_todos.active mount 'TestComponent2', todos: todos do class TestComponent2 < React::Component::Base param :todos, type: [Todo] render(:div) do P { "params.todos = #{params.todos.count}" } P { "params.user.authored_todos.active = #{User.find(1).authored_todos.active.count}" } end end end page.should have_content('params.todos = 5') page.should have_content('params.user.authored_todos.active = 5') end context 'basic joins' do it 'will not update a joined scope without a joins option' do isomorphic do TestModel.class_eval do scope :joined, -> { joins(:child_models).where("child_attribute = 'WHAAA'") } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base render { "TestModel.joined.count = #{TestModel.joined.count}" } end end parent = FactoryGirl.create(:test_model) child = FactoryGirl.create(:child_model, test_model: parent ) page.should have_content('.count = 0') child.update_attribute(:child_attribute, 'WHAAA') page.should have_content('.count = 0') end it 'can have a joins option' do isomorphic do TestModel.class_eval do scope :joined, -> { joins(:child_models).where("child_attribute = 'WHAAA'") }, joins: 'child_models' end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base render { "TestModel.joined.count = #{TestModel.joined.count}" } end end parent = FactoryGirl.create(:test_model) child = FactoryGirl.create(:child_model, test_model: parent ) page.should have_content('.count = 0') child.update_attribute(:child_attribute, 'WHAAA') page.should have_content('.count = 1') end end it 'can have a client filter method' do isomorphic do TestModel.class_eval do scope :quicker, -> { where(completed: true) }, client: -> { completed } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base before_mount do @render_count = 1 end before_update do @render_count = @render_count + 1 end render(:div) do div { "rendered #{@render_count} times"} div { "quicker.count = #{TestModel.quicker.count}" } end end end m1 = FactoryGirl.create(:test_model) page.should have_content('.count = 0') page.should have_content('rendered 2 times') TestModel.quicker_count.should eq(1) m1.update_attribute(:test_attribute, 'new_value') wait_for_ajax page.should have_content('.count = 0') page.should have_content('rendered 2 times') TestModel.quicker_count.should eq(1) m1.update_attribute(:completed, true) page.should have_content('.count = 1') page.should have_content('rendered 3 times') TestModel.quicker_count.should eq(1) m2 = FactoryGirl.create(:test_model, completed: true) page.should have_content('.count = 2') page.should have_content('rendered 4 times') TestModel.quicker_count.should eq(1) m1.destroy page.should have_content('.count = 1') page.should have_content('rendered 5 times') TestModel.quicker_count.should eq(1) end it 'can have a client collector method' do isomorphic do TestModel.class_eval do def <=>(other); self.test_attribute <=> other.test_attribute; end scope :filter_and_sort, -> { where(completed: true).order('test_attribute ASC') }, select: -> { select { |r| r.completed }.sort } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base before_mount do @render_count = 1 end before_update do @render_count = @render_count + 1 end render(:div) do div { "rendered #{@render_count} times"} div { "filter_and_sort.count = #{TestModel.filter_and_sort.count}" } if TestModel.filter_and_sort.any? div { "test attributes: #{TestModel.filter_and_sort.collect { |r| r.test_attribute }.join(', ')}" } else div { "no test attributes" } end end end end TestModel.filter_and_sort_count.should eq(2) m1 = FactoryGirl.create(:test_model) page.should have_content('.count = 0') page.should have_content('rendered 2 times') page.should have_content('no test attributes') TestModel.filter_and_sort_count.should eq(2) m1.update_attribute(:test_attribute, 'N') wait_for_ajax page.should have_content('.count = 0') page.should have_content('rendered 2 times') page.should have_content('no test attributes') TestModel.filter_and_sort_count.should eq(2) m1.update_attribute(:completed, true) page.should have_content('.count = 1') page.should have_content('rendered 3 times') page.should have_content('test attributes: N') TestModel.filter_and_sort_count.should eq(2) m2 = FactoryGirl.create(:test_model, test_attribute: 'A', completed: true) page.should have_content('.count = 2') page.should have_content('rendered 4 times') page.should have_content('test attributes: A, N') TestModel.filter_and_sort_count.should eq(2) m3 = FactoryGirl.create(:test_model, test_attribute: 'Z', completed: true) page.should have_content('.count = 3') page.should have_content('rendered 5 times') page.should have_content('test attributes: A, N, Z') TestModel.filter_and_sort_count.should eq(2) m1.destroy page.should have_content('.count = 2') page.should have_content('rendered 6 times') page.should have_content('test attributes: A, Z') TestModel.filter_and_sort_count.should eq(2) m3.update_attribute(:completed, false) page.should have_content('.count = 1') page.should have_content('rendered 7 times') page.should have_content('test attributes: A') TestModel.filter_and_sort_count.should eq(2) end it 'client side scoping methods can take args' do isomorphic do TestModel.class_eval do def to_s "" end def <=>(other) (test_attribute || '') <=> (other.test_attribute || '') end scope :matches, -> (s) { where(['test_attribute LIKE ?', "%#{s}%"])}, client: -> (s) { puts "test_attribute =~ /#{s}/ is #{!!(test_attribute =~ /#{s}/)}"; test_attribute =~ /#{s}/ } scope :sorted, -> (dir) { order("test_attribute #{dir==:asc ? 'ASC' : 'DESC'}") }, select: -> (dir) { sort { |a, b| dir == :asc ? a <=> b : b <=> a }.tap {|r| puts "sorted(#{dir==:asc}) as [#{r}]"} } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base export_state pat: :foo export_state dir: :asc before_mount do @render_count = 1 end before_update do @render_count = @render_count + 1 end def scoped puts "scoping: #{TestComponent2.pat}, #{TestComponent2.dir}" TestModel.matches(TestComponent2.pat).sorted(TestComponent2.dir) end render(:div) do div { "rendered #{@render_count} times"} div { "matches(#{TestComponent2.pat}).count = #{TestModel.matches(TestComponent2.pat).count}" } if scoped.any? div { "test attributes: #{scoped.collect { |r| r.test_attribute[0] }.join(', ')}" } else div { "no test attributes" } end end end end starting_fetch_time = evaluate_ruby("ReactiveRecord::Base.last_fetch_at") m1 = FactoryGirl.create(:test_model) page.should have_content('.count = 0') page.should have_content('rendered 2 times') page.should have_content('no test attributes') m1.update_attribute(:test_attribute, 'N') page.should have_content('rendered 2 times') page.should have_content('.count = 0') page.should have_content('no test attributes') m1.update_attribute(:test_attribute, 'N - foobar') page.should have_content('.count = 1') page.should have_content('rendered 3 times') page.should have_content('test attributes: N') m2 = FactoryGirl.create(:test_model, test_attribute: 'A - is a foo') page.should have_content('.count = 2') page.should have_content('rendered 4 times') page.should have_content('test attributes: A, N') m3 = FactoryGirl.create(:test_model, test_attribute: 'Z - is also a foo') page.should have_content('.count = 3') page.should have_content('rendered 5 times') page.should have_content('test attributes: A, N, Z') evaluate_ruby("ReactiveRecord::Base.last_fetch_at").should eq(starting_fetch_time) evaluate_ruby("TestComponent2.dir! :desc") page.should have_content('test attributes: Z, N, A') page.should have_content('rendered 7 times') starting_fetch_time = evaluate_ruby("ReactiveRecord::Base.last_fetch_at") m1.destroy page.should have_content('.count = 2') page.should have_content('rendered 8 times') page.should have_content('test attributes: Z, A') m3.update_attribute(:test_attribute, 'Z - is a bar') page.should have_content('.count = 1') page.should have_content('rendered 9 times') page.should have_content('test attributes: A') evaluate_ruby("ReactiveRecord::Base.last_fetch_at").should eq(starting_fetch_time) evaluate_ruby('TestComponent2.pat! :bar') page.should have_content('.count = 1') page.should have_content('test attributes: Z') page.should have_content('rendered 11 times') starting_fetch_time = evaluate_ruby("ReactiveRecord::Base.last_fetch_at") m2.update_attribute(:test_attribute, 'A is also a bar') page.should have_content('.count = 2') page.should have_content('rendered 12 times') page.should have_content('test attributes: Z, A') evaluate_ruby("ReactiveRecord::Base.last_fetch_at").should eq(starting_fetch_time) end it 'the joins array can be combined with the client proc' do isomorphic do TestModel.class_eval do scope :has_children, joins: 'child_models', server: -> { joins(:child_models).distinct }, client: -> { child_models.count > 0 } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base render { "TestModel.has_children.count = #{TestModel.has_children.count}" } end end TestModel.has_children_count.should eq(1) parent = FactoryGirl.create(:test_model) page.should have_content('.count = 0') child = FactoryGirl.create(:child_model) page.should have_content('.count = 0') child.update_attribute(:test_model, parent) page.should have_content('.count = 1') child.update_attribute(:child_attribute, 'WHAAA') page.should have_content('.count = 1') child.update_attribute(:test_model, nil) page.should have_content('.count = 0') child.update_attribute(:test_model, parent) page.should have_content('.count = 1') child.destroy page.should have_content('.count = 0') parent.destroy page.should have_content('.count = 0') TestModel.has_children_count.should eq(1) end it 'the joins option can join with all or no models' do isomorphic do TestModel.class_eval do scope :has_children_joins_all, joins: :all, server: -> { joins(:child_models).distinct } scope :has_children_no_joins, joins: [], server: -> { joins(:child_models).distinct } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base render(DIV) do DIV { "TestModel.has_children_joins_all.count = #{TestModel.has_children_joins_all.count}" } DIV { "TestModel.has_children_no_joins.count = #{TestModel.has_children_no_joins.count}" } end end end TestModel.has_children_joins_all_count.should eq(1) TestModel.has_children_no_joins_count.should eq(1) parent = FactoryGirl.create(:test_model) wait_for_ajax TestModel.has_children_no_joins_count.should eq(1) wait_for { TestModel.has_children_joins_all_count }.to eq(2) child = FactoryGirl.create(:child_model) wait_for { TestModel.has_children_joins_all_count }.to eq(3) parent.child_models << child wait_for { TestModel.has_children_joins_all_count }.to eq(4) TestModel.has_children_no_joins_count.should eq(1) page.should have_content('all.count = 1') page.should have_content('joins.count = 0') parent.update_attribute(:test_attribute, 'hello') page.should have_content('joins.count = 0') TestModel.has_children_no_joins_count.should eq(1) end it 'server side scopes can be procs returning arrays of elements' do isomorphic do TestModel.class_eval do scope :rev, server: -> { all.reverse } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base render do "test attributes: #{TestModel.rev.collect { |r| r.test_attribute }.join(', ')}" end end end FactoryGirl.create(:test_model, test_attribute: 1) page.should have_content('test attributes: 1') FactoryGirl.create(:test_model, test_attribute: 2) page.should have_content('test attributes: 2, 1') end it 'can force reload using the ! suffix' do isomorphic do TestModel.class_eval do scope :rev, joins: [], server: -> { all.reverse } end end mount 'TestComponent2' do class TestComponent2 < React::Component::Base before_mount do @reverse_collection = [] end render do if @current_count != TestModel.all.count # this forces a rerender... puts "asking for reverse collection" @reverse_collection = TestModel.rev! @current_count = TestModel.all.count end puts "rendering #{@reverse_collection}" "test attributes: #{@reverse_collection.collect { |r| r.test_attribute }.join(', ')}" end end end FactoryGirl.create(:test_model, test_attribute: 1) page.should have_content('test attributes: 1') FactoryGirl.create(:test_model, test_attribute: 2) page.should have_content('test attributes: 2, 1') end end