|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require_relative "test_helper" |
| 4 | +require "mocha/minitest" |
| 5 | + |
| 6 | +class StimulusReflex::ReflexTargetsTest < ActionCable::Channel::TestCase |
| 7 | + tests StimulusReflex::Channel |
| 8 | + |
| 9 | + attr_reader :reflex |
| 10 | + delegate :post_targets, :button_target, :absent_target, :unicorn_targets, :cable_ready, to: :reflex |
| 11 | + |
| 12 | + setup do |
| 13 | + stub_connection(session_id: SecureRandom.uuid) |
| 14 | + def connection.env |
| 15 | + @env ||= {} |
| 16 | + end |
| 17 | + |
| 18 | + @element = StimulusReflex::Element.new({}, selector: "/html/body/button[1]") |
| 19 | + @reflex = build_with_targets |
| 20 | + end |
| 21 | + |
| 22 | + def build_with_targets(targets_data: nil, target_scope: "page") |
| 23 | + targets_data ||= { |
| 24 | + "post" => [ |
| 25 | + { "name" => "post", "selector" => "/html/body/div[1]", "attrs" => { "class" => "" } }, |
| 26 | + { "name" => "post", "selector" => "/html/body/div[2]", "attrs" => { "class" => "special" } }, |
| 27 | + { "name" => "post", "selector" => "/html/body/div[3]", "attrs" => { "class" => "special" } } |
| 28 | + ], |
| 29 | + "button" => [ |
| 30 | + { "name" => "button", "selector" => "/html/body/button[1]", "dataset" => {} } |
| 31 | + ] |
| 32 | + } |
| 33 | + |
| 34 | + reflex_data = StimulusReflex::ReflexData.new( |
| 35 | + element: @element, |
| 36 | + url: "https://test.stimulusreflex.com", |
| 37 | + targets: targets_data, |
| 38 | + id: "123", |
| 39 | + version: StimulusReflex::VERSION, |
| 40 | + reflex_controller: "stimulus-reflex", |
| 41 | + target_scope: target_scope |
| 42 | + ) |
| 43 | + |
| 44 | + StimulusReflex::Reflex.new(subscribe, reflex_data: reflex_data) |
| 45 | + end |
| 46 | + |
| 47 | + def build_payload(operations = []) |
| 48 | + { |
| 49 | + "cableReady" => true, |
| 50 | + "operations" => Array.wrap(operations), |
| 51 | + "version" => CableReady::VERSION |
| 52 | + } |
| 53 | + end |
| 54 | + |
| 55 | + test "shares a cable_ready instance with targets and target collections" do |
| 56 | + assert_equal reflex.cable_ready, button_target.cable_ready |
| 57 | + assert_equal reflex.cable_ready, post_targets.cable_ready |
| 58 | + assert_equal reflex.cable_ready, post_targets.first.cable_ready |
| 59 | + end |
| 60 | + |
| 61 | + test "builds chainable operations on a (singular) target" do |
| 62 | + expected = build_payload( |
| 63 | + [ |
| 64 | + {"selector" => "/html/body/button[1]", "xpath" => true, "name" => "updated", "reflexId" => "123", "operation" => "addCssClass"}, |
| 65 | + {"selector" => "/html/body/button[1]", "xpath" => true, "text" => "Button", "reflexId" => "123", "operation" => "textContent"} |
| 66 | + ] |
| 67 | + ) |
| 68 | + |
| 69 | + assert_broadcast_on(reflex.stream_name, expected) do |
| 70 | + button_target.add_css_class(name: "updated").text_content(text: "Button") |
| 71 | + |
| 72 | + reflex.cable_ready.broadcast |
| 73 | + end |
| 74 | + end |
| 75 | + |
| 76 | + test "builds chainable operations on (plural) multi-target collection using select_all" do |
| 77 | + expected = build_payload( |
| 78 | + [ |
| 79 | + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "name" => "updated", "reflexId" => "123", "operation" => "addCssClass"}, |
| 80 | + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "text" => "Post", "reflexId" => "123", "operation" => "textContent"} |
| 81 | + ] |
| 82 | + ) |
| 83 | + |
| 84 | + assert_broadcast_on(reflex.stream_name, expected) do |
| 85 | + post_targets.add_css_class(name: "updated").text_content(text: "Post") |
| 86 | + |
| 87 | + reflex.cable_ready.broadcast |
| 88 | + end |
| 89 | + end |
| 90 | + |
| 91 | + test "target collections respond to array-like interface" do |
| 92 | + assert_equal post_targets.any?, true |
| 93 | + assert_equal post_targets.many?, true |
| 94 | + assert_equal post_targets.count, 3 |
| 95 | + assert_equal post_targets.first.selector, "/html/body/div[1]" |
| 96 | + assert_equal post_targets.last.selector, "/html/body/div[3]" |
| 97 | + |
| 98 | + special_targets = post_targets.select{ |target| target.attrs[:class].include?("special") } |
| 99 | + |
| 100 | + assert_equal special_targets.count, 2 |
| 101 | + assert_equal special_targets.first.selector, "/html/body/div[2]" |
| 102 | + assert_equal special_targets.last.selector, "/html/body/div[3]" |
| 103 | + end |
| 104 | + |
| 105 | + test "doesn't raise an exception / halt execution if operation(s) are called on a missing target" do |
| 106 | + expected = build_payload( |
| 107 | + [ |
| 108 | + {"selector" => "/html/body/button[1]", "xpath" => true, "name" => "success", "reflexId" => "123", "operation" => "addCssClass"}, |
| 109 | + {"selector" => "/html/body/button[1]", "xpath" => true, "text" => "I'm still updated!", "reflexId" => "123", "operation" => "textContent"} |
| 110 | + ] |
| 111 | + ) |
| 112 | + |
| 113 | + assert_broadcast_on(reflex.stream_name, expected) do |
| 114 | + absent_target.add_css_class(name: "nope").text_content(text: "I'm not even here!") |
| 115 | + button_target.add_css_class(name: "success").text_content(text: "I'm still updated!") |
| 116 | + |
| 117 | + reflex.cable_ready.broadcast |
| 118 | + end |
| 119 | + end |
| 120 | + |
| 121 | + test "missing/undefined targets that *might* exist but are currently not in the DOM still respond to inspection" do |
| 122 | + assert_equal absent_target.any?, false |
| 123 | + assert_equal absent_target.present?, false |
| 124 | + assert_equal unicorn_targets.count, 0 |
| 125 | + assert_equal unicorn_targets.first.present?, false |
| 126 | + assert_equal unicorn_targets.any?, false |
| 127 | + end |
| 128 | + |
| 129 | + test "targets in a multi-target collection can also be operated on individually" do |
| 130 | + expected = build_payload( |
| 131 | + [ |
| 132 | + {"selector" => "/html/body/div[2]", "xpath" => true, "name" => "upgrade", "reflexId" => "123", "operation" => "addCssClass"}, |
| 133 | + {"selector" => "/html/body/div[3]", "xpath" => true, "name" => "upgrade", "reflexId" => "123", "operation" => "addCssClass"}, |
| 134 | + {"selector" => "/html/body/div[1]", "xpath" => true, "name" => "downgrade", "reflexId" => "123", "operation" => "addCssClass"} |
| 135 | + ] |
| 136 | + ) |
| 137 | + |
| 138 | + assert_broadcast_on(reflex.stream_name, expected) do |
| 139 | + post_targets |
| 140 | + .select{ |target| target.attrs[:class].include?("special") } |
| 141 | + .each{ |target| target.add_css_class(name: "upgrade") } |
| 142 | + |
| 143 | + post_targets |
| 144 | + .find{ |target| target.attrs[:class].blank? } |
| 145 | + .add_css_class(name: "downgrade") |
| 146 | + |
| 147 | + reflex.cable_ready.broadcast |
| 148 | + end |
| 149 | + end |
| 150 | + |
| 151 | + test "plays nicely with other operations interspersed" do |
| 152 | + expected = build_payload( |
| 153 | + [ |
| 154 | + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "name" => "hey", "reflexId" => "123", "operation" => "addCssClass"}, |
| 155 | + {"selector" => "#other", "name" => "thing", "reflexId" => "123", "operation" => "addCssClass"}, |
| 156 | + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "text" => "I'm a Post", "reflexId" => "123", "operation" => "textContent"} |
| 157 | + ] |
| 158 | + ) |
| 159 | + |
| 160 | + assert_broadcast_on(reflex.stream_name, expected) do |
| 161 | + post_targets.add_css_class(name: "hey") |
| 162 | + cable_ready.add_css_class(selector: "#other", name: "thing") |
| 163 | + post_targets.text_content(text: "I'm a Post") |
| 164 | + |
| 165 | + reflex.cable_ready.broadcast |
| 166 | + end |
| 167 | + end |
| 168 | +end |
0 commit comments