From 4ab5b6051fd7f560ac4617861e120b2552ad74c0 Mon Sep 17 00:00:00 2001 From: Matt-Yorkley <9029026+Matt-Yorkley@users.noreply.github.com> Date: Wed, 31 May 2023 15:57:48 +0100 Subject: [PATCH] Add tests for targets and target collections --- .../test/attributes.extractTargets.test.js | 58 ++++++ test/reflex_targets_test.rb | 168 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 javascript/test/attributes.extractTargets.test.js create mode 100644 test/reflex_targets_test.rb diff --git a/javascript/test/attributes.extractTargets.test.js b/javascript/test/attributes.extractTargets.test.js new file mode 100644 index 00000000..6a7bd715 --- /dev/null +++ b/javascript/test/attributes.extractTargets.test.js @@ -0,0 +1,58 @@ +import { html, fixture, assert } from '@open-wc/testing' + +import { extractTargets } from '../attributes' +import Schema, { defaultSchema } from '../schema' + +Schema.set(defaultSchema) + +describe('extractTargets', () => { + it('should extract no targets by default', async () => { + const dom = await fixture( + html`
Post
` + ) + const targets = extractTargets(undefined, null) + assert.deepStrictEqual(targets, {}) + }) + + it('should extract multiple targets from page', async () => { + const dom = await fixture( + html` +
Post
+
Comment 1
+
Comment 2
+ ` + ) + const targets = extractTargets("page", null) + + assert.equal(targets["post"][0]["name"], "post") + assert.equal(targets["post"][0]["selector"], "/html/body/div[1]/div[1]") + assert.equal(targets["post"][0]["attrs"]["data-reflex-target"], "post") + + assert.equal(targets["comment"][0]["name"], "comment") + assert.equal(targets["comment"][0]["selector"], "/html/body/div[1]/div[2]") + assert.equal(targets["comment"][0]["attrs"]["data-reflex-target"], "comment") + assert.equal(targets["comment"][0]["attrs"]["class"], "comment-1") + + assert.equal(targets["comment"][1]["name"], "comment") + assert.equal(targets["comment"][1]["selector"], "/html/body/div[1]/div[3]") + assert.equal(targets["comment"][1]["attrs"]["data-reflex-target"], "comment") + assert.equal(targets["comment"][1]["attrs"]["class"], "comment-2") + }) + + it('should limit targets to parent controller if specified', async () => { + const controller = await fixture( // Note: fixture() returns the first element in the DOM + html` +
+
In
+
+
Out
+ ` + ) + + const targets = extractTargets("controller", controller) + + assert.equal(targets["included"][0]["name"], "included") + assert.equal(targets["included"][0]["selector"], "/html/body/div[1]/div[1]/div[1]") + assert.equal(targets["not_included"], undefined) + }) +}) diff --git a/test/reflex_targets_test.rb b/test/reflex_targets_test.rb new file mode 100644 index 00000000..d8c5d09d --- /dev/null +++ b/test/reflex_targets_test.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "mocha/minitest" + +class StimulusReflex::ReflexTargetsTest < ActionCable::Channel::TestCase + tests StimulusReflex::Channel + + attr_reader :reflex + delegate :post_targets, :button_target, :absent_target, :unicorn_targets, :cable_ready, to: :reflex + + setup do + stub_connection(session_id: SecureRandom.uuid) + def connection.env + @env ||= {} + end + + @element = StimulusReflex::Element.new({}, selector: "/html/body/button[1]") + @reflex = build_with_targets + end + + def build_with_targets(targets_data: nil, target_scope: "page") + targets_data ||= { + "post" => [ + { "name" => "post", "selector" => "/html/body/div[1]", "attrs" => { "class" => "" } }, + { "name" => "post", "selector" => "/html/body/div[2]", "attrs" => { "class" => "special" } }, + { "name" => "post", "selector" => "/html/body/div[3]", "attrs" => { "class" => "special" } } + ], + "button" => [ + { "name" => "button", "selector" => "/html/body/button[1]", "dataset" => {} } + ] + } + + reflex_data = StimulusReflex::ReflexData.new( + element: @element, + url: "https://test.stimulusreflex.com", + targets: targets_data, + id: "123", + version: StimulusReflex::VERSION, + reflex_controller: "stimulus-reflex", + target_scope: target_scope + ) + + StimulusReflex::Reflex.new(subscribe, reflex_data: reflex_data) + end + + def build_payload(operations = []) + { + "cableReady" => true, + "operations" => Array.wrap(operations), + "version" => CableReady::VERSION + } + end + + test "shares a cable_ready instance with targets and target collections" do + assert_equal reflex.cable_ready, button_target.cable_ready + assert_equal reflex.cable_ready, post_targets.cable_ready + assert_equal reflex.cable_ready, post_targets.first.cable_ready + end + + test "builds chainable operations on a (singular) target" do + expected = build_payload( + [ + {"selector" => "/html/body/button[1]", "xpath" => true, "name" => "updated", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "/html/body/button[1]", "xpath" => true, "text" => "Button", "reflexId" => "123", "operation" => "textContent"} + ] + ) + + assert_broadcast_on(reflex.stream_name, expected) do + button_target.add_css_class(name: "updated").text_content(text: "Button") + + reflex.cable_ready.broadcast + end + end + + test "builds chainable operations on (plural) multi-target collection using select_all" do + expected = build_payload( + [ + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "name" => "updated", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "text" => "Post", "reflexId" => "123", "operation" => "textContent"} + ] + ) + + assert_broadcast_on(reflex.stream_name, expected) do + post_targets.add_css_class(name: "updated").text_content(text: "Post") + + reflex.cable_ready.broadcast + end + end + + test "target collections respond to array-like interface" do + assert_equal post_targets.any?, true + assert_equal post_targets.many?, true + assert_equal post_targets.count, 3 + assert_equal post_targets.first.selector, "/html/body/div[1]" + assert_equal post_targets.last.selector, "/html/body/div[3]" + + special_targets = post_targets.select{ |target| target.attrs[:class].include?("special") } + + assert_equal special_targets.count, 2 + assert_equal special_targets.first.selector, "/html/body/div[2]" + assert_equal special_targets.last.selector, "/html/body/div[3]" + end + + test "doesn't raise an exception / halt execution if operation(s) are called on a missing target" do + expected = build_payload( + [ + {"selector" => "/html/body/button[1]", "xpath" => true, "name" => "success", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "/html/body/button[1]", "xpath" => true, "text" => "I'm still updated!", "reflexId" => "123", "operation" => "textContent"} + ] + ) + + assert_broadcast_on(reflex.stream_name, expected) do + absent_target.add_css_class(name: "nope").text_content(text: "I'm not even here!") + button_target.add_css_class(name: "success").text_content(text: "I'm still updated!") + + reflex.cable_ready.broadcast + end + end + + test "missing/undefined targets that *might* exist but are currently not in the DOM still respond to inspection" do + assert_equal absent_target.any?, false + assert_equal absent_target.present?, false + assert_equal unicorn_targets.count, 0 + assert_equal unicorn_targets.first.present?, false + assert_equal unicorn_targets.any?, false + end + + test "targets in a multi-target collection can also be operated on individually" do + expected = build_payload( + [ + {"selector" => "/html/body/div[2]", "xpath" => true, "name" => "upgrade", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "/html/body/div[3]", "xpath" => true, "name" => "upgrade", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "/html/body/div[1]", "xpath" => true, "name" => "downgrade", "reflexId" => "123", "operation" => "addCssClass"} + ] + ) + + assert_broadcast_on(reflex.stream_name, expected) do + post_targets + .select{ |target| target.attrs[:class].include?("special") } + .each{ |target| target.add_css_class(name: "upgrade") } + + post_targets + .find{ |target| target.attrs[:class].blank? } + .add_css_class(name: "downgrade") + + reflex.cable_ready.broadcast + end + end + + test "plays nicely with other operations interspersed" do + expected = build_payload( + [ + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "name" => "hey", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "#other", "name" => "thing", "reflexId" => "123", "operation" => "addCssClass"}, + {"selector" => "[data-reflex-target='post']", "selectAll" => true, "text" => "I'm a Post", "reflexId" => "123", "operation" => "textContent"} + ] + ) + + assert_broadcast_on(reflex.stream_name, expected) do + post_targets.add_css_class(name: "hey") + cable_ready.add_css_class(selector: "#other", name: "thing") + post_targets.text_content(text: "I'm a Post") + + reflex.cable_ready.broadcast + end + end +end