phlex-reactive 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -730,7 +730,13 @@ module Phlex
730
730
  action: "input->reactive#recompute",
731
731
  reactive_compute_reducer_param: definition.reducer,
732
732
  reactive_compute_inputs_param: compute_inputs_param(definition),
733
- reactive_compute_outputs_param: definition.outputs.map(&:to_s).to_json
733
+ reactive_compute_outputs_param: definition.outputs.map(&:to_s).to_json,
734
+ # Issue #199: the client self-seeds the derived fields on connect from
735
+ # this marker, so a freshly-rendered compute root computes its outputs
736
+ # + mirrors on first paint — no wait for the first user input, and no
737
+ # synthetic seed `input` for an app to race. STRING "true" (a valueless
738
+ # boolean attr renders "" → falsy client-side; the client reads == "true").
739
+ reactive_compute_seed: "true"
734
740
  }
735
741
  # Declared cross-root text mirrors (issue #159) ride as a JSON object of
736
742
  # name → [id selectors]; omitted entirely when undeclared so the shipped
@@ -106,7 +106,11 @@ module Phlex
106
106
  return false unless result.success?
107
107
 
108
108
  names = authorization_method_names
109
- call_names(result.value).any? { names.include?(it) }
109
+ # Set#intersect? on the Set receiver (names) — Array#intersect? would raise
110
+ # TypeError on a Set arg (caught by the rescue below → a silent false, the
111
+ # bug the naive Style/ArrayIntersect autocorrect introduces). Set#intersect?
112
+ # accepts any Enumerable, so the call_names Array is a valid argument.
113
+ names.intersect?(call_names(result.value))
110
114
  rescue StandardError
111
115
  false
112
116
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # System/browser test helpers for reactive components (issue #201). Loaded lazily
4
+ # by Phlex::Reactive::TestHelpers ONLY when Capybara is present (the pgbus/mcp
5
+ # optional-require precedent), so the gem never hard depends on Capybara and this
6
+ # module stays out of the request/unit path.
7
+ #
8
+ # Mix it into your system examples from rails_helper:
9
+ #
10
+ # RSpec.configure do |c|
11
+ # c.include Phlex::Reactive::TestHelpers::System, type: :system
12
+ # end
13
+ #
14
+ # It is built on the GLOBAL reactive-activity signal the client runtime exposes:
15
+ # a <html data-reactive-active> marker present while ANY reactive operation (a
16
+ # dispatch round trip OR a deferred render) is in flight, cleared when the whole
17
+ # layer settles. That is the clean primitive to wait on — instead of each spec
18
+ # scraping the union of per-root busy/pending/seed selectors, or holding a node
19
+ # that a morph detaches (StaleReferenceError).
20
+ #
21
+ # wait_for_reactive # block until the layer is idle
22
+ # expect(page).to have_reactive_value("total", "6") # a field, re-resolved each poll
23
+ # expect(page).to have_reactive_text("recap", "6 items") # a mirror node, re-resolved
24
+ #
25
+ # All three re-resolve the DOM every poll cycle (Capybara's waiting behavior), so
26
+ # a compute re-seed / morph / in-flight round trip that REPLACES the node after
27
+ # the triggering action returns can never surface a StaleReferenceError or read a
28
+ # transient blank — the matcher waits for the value to SETTLE.
29
+ module Phlex
30
+ module Reactive
31
+ module TestHelpers
32
+ module System
33
+ # The <html> marker the client sets while the reactive layer is busy. Kept
34
+ # in lockstep with reactive_controller.js's ACTIVE_ATTR — the ONE selector
35
+ # every wait keys off. A [data-reactive-active] presence check is the system
36
+ # twin of wait_for_turbo watching the Turbo progress bar.
37
+ ACTIVE_MARKER = "data-reactive-active"
38
+
39
+ # Block until the reactive layer is IDLE — every dispatch round trip and
40
+ # deferred render has settled and the <html data-reactive-active> marker is
41
+ # gone. The system-test twin of wait_for_turbo (which watches the Turbo
42
+ # progress bar, NOT a reactive morph/seed, so it can't cover this).
43
+ #
44
+ # Implemented as a Capybara WAITING assertion (have_no_css on the document
45
+ # element with the default max wait), so it re-checks the live DOM each poll
46
+ # and raises a readable Capybara::ElementNotFound-style error if the layer
47
+ # never settles inside `timeout` — never a bare sleep, never a stale read.
48
+ #
49
+ # `timeout:` overrides Capybara.default_max_wait_time for a slow operation
50
+ # (a deferred render behind a real job). Returns nil; call it as a barrier
51
+ # BEFORE asserting a settled value if you are not already using one of the
52
+ # waiting matchers below.
53
+ def wait_for_reactive(timeout: nil)
54
+ # Scope the check to <html> via the :xpath "/html" so the marker is read
55
+ # on the document element the client writes it to — not a descendant.
56
+ # assert_no_selector WAITS (retries) until the marker clears or the wait
57
+ # budget elapses; a persistent marker fails LOUDLY with Capybara's own
58
+ # timeout error rather than a silent pass. Called on `page` (the current
59
+ # session) so it works regardless of whether the example group mixed in
60
+ # Capybara::DSL.
61
+ page.assert_no_selector(:xpath, "/html[@#{ACTIVE_MARKER}]", **wait_option(timeout))
62
+ nil
63
+ end
64
+
65
+ # Assert (waiting) that the field with DOM id `id` has value `value`,
66
+ # RE-RESOLVING the field by its id on every poll. Use it as the barrier for
67
+ # a value that settles a beat after the triggering action — a reactive_compute
68
+ # re-seed or a morph replaces the input node AFTER the action returns, so a
69
+ # node captured by `find` would go stale; keying on the id and letting
70
+ # Capybara re-query each cycle is immune to that.
71
+ #
72
+ # expect(page).to have_reactive_value("total", "6")
73
+ #
74
+ # A thin, intention-revealing wrapper over Capybara's field matcher scoped
75
+ # by id — the value the app is waiting on, named for what it is. The
76
+ # `have_` prefix is Capybara-matcher convention (have_field/have_css), NOT a
77
+ # predicate — hence the PredicatePrefix disable, mirroring matchers.rb.
78
+ # rubocop:disable Naming/PredicatePrefix
79
+ def have_reactive_value(id, value, **)
80
+ have_field(id, with: value, **)
81
+ end
82
+
83
+ # Assert (waiting) that the node with DOM id `id` has TEXT `value`,
84
+ # re-resolving by id each poll — the mirror/recap twin of
85
+ # have_reactive_value for a text sink (a reactive_compute `text:`/mirror
86
+ # target, a recap node) rather than a form field.
87
+ #
88
+ # expect(page).to have_reactive_text("recap", "6 items")
89
+ def have_reactive_text(id, value, **)
90
+ have_css("##{id}", text: value, **)
91
+ end
92
+ # rubocop:enable Naming/PredicatePrefix
93
+
94
+ private
95
+
96
+ # Fold an explicit `timeout:` into Capybara's `wait:` option, or omit it so
97
+ # Capybara's configured default applies. Kept tiny so every waiter shares
98
+ # one timeout convention.
99
+ def wait_option(timeout)
100
+ timeout.nil? ? {} : { wait: timeout }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -314,3 +314,9 @@ end
314
314
  # them only when RSpec is present, so a Minitest app can mix in TestHelpers and
315
315
  # assert on Result predicates directly with no RSpec dependency.
316
316
  require "phlex/reactive/test_helpers/matchers" if defined?(RSpec::Matchers)
317
+
318
+ # The system/browser helpers (wait_for_reactive + the re-resolving value/text
319
+ # matchers, issue #201) are optional too: load them only when Capybara is present
320
+ # (a dev/test dependency), keeping them entirely out of the runtime path — the
321
+ # same optional-require gate as the matchers above and the engine below.
322
+ require "phlex/reactive/test_helpers/system" if defined?(Capybara)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.11.0"
5
+ VERSION = "0.11.2"
6
6
  end
7
7
  end
@@ -978,6 +978,11 @@ loader.ignore("#{lib}/generators")
978
978
  # Zeitwerk's naming — test_helpers.rb requires it explicitly (only when RSpec is
979
979
  # present), and the loader must ignore it.
980
980
  loader.ignore("#{lib}/phlex/reactive/test_helpers/matchers.rb")
981
+ # The system/browser test helpers (issue #201) are Capybara-only — a dev/test
982
+ # dependency, never a runtime one. test_helpers.rb requires this file explicitly
983
+ # only when Capybara is present, so the loader must ignore it (otherwise an
984
+ # eager-load in production would define browser helpers with no Capybara).
985
+ loader.ignore("#{lib}/phlex/reactive/test_helpers/system.rb")
981
986
  # The MCP diagnostic tool tree (issue #168) subclasses the OPTIONAL `mcp` gem's
982
987
  # constants (MCP::Tool) at class-definition time, so the whole mcp/ subdirectory
983
988
  # must stay out of the autoloader — Phlex::Reactive::MCP.load! requires it in
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -177,6 +177,7 @@ files:
177
177
  - lib/phlex/reactive/streamable.rb
178
178
  - lib/phlex/reactive/test_helpers.rb
179
179
  - lib/phlex/reactive/test_helpers/matchers.rb
180
+ - lib/phlex/reactive/test_helpers/system.rb
180
181
  - lib/phlex/reactive/version.rb
181
182
  - lib/tasks/phlex_reactive.rake
182
183
  homepage: https://github.com/mhenrixon/phlex-reactive