phlex-reactive 0.11.2 → 0.11.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac1f001e762b40825811633d86a7cb9e050b85d32a7f7c11a44196b48f486af6
4
- data.tar.gz: a969a678bf9f0d293b953529f9c31eb4871263dc391477e4301d3fedd5cec6fb
3
+ metadata.gz: 23b25b41281afc59503c63e9e4fbcee696d1265b96682c1c97bfd0adf385055d
4
+ data.tar.gz: 7204885d357720c85e57c8b44192d6e000f91c985346177a7ddcefc28f7451cc
5
5
  SHA512:
6
- metadata.gz: 8378be9d18bd169e2f78dfbd5171931e952ecbb875c74df0ba5328518ea0a74c641e7840807a7d6eb216d647ecd0daa6f32cdf70468d049e6ed47fa2d0169903
7
- data.tar.gz: 7ec7fb3754b137191feb9d38dca9ef9b14b6993da9909dd10ea0f25b661660259c3839b4fa011982dec206d687b765b0a70e7d762ca62c09ed22cbf7f055dba0
6
+ metadata.gz: 2afb0dfef67df5cc60f176db516ca7c4ed5553e7f24a9df9ff098dc7a897e1085594c698661d437f8ea336b4eab5d6bc76b884073f0f52b37b09a8f5fe4bd711
7
+ data.tar.gz: 411a32490ecdc9224d18e5db37b443286d1d3e47210e733e2cb9e6083aac5af2ac5ead6f96e54ab1cf0629483addd3f888de209976a6b2161259c5a6d9c0a93e
data/CHANGELOG.md CHANGED
@@ -47,6 +47,16 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
47
47
 
48
48
  ### Fixed
49
49
 
50
+ - **`have_reactive_value` now reads the field's `.value` property, so it can verify a
51
+ reducer-set disabled/computed field (#204).** The matcher (added in #201) delegated to
52
+ Capybara's `have_field(with:)`, which matches the value **attribute** — but a
53
+ `reactive_compute` reducer paints a computed output by setting its JS `.value`
54
+ **property** (`el.value = …`), and for a **disabled / read-only** output the attribute
55
+ never reflects that. So the matcher read `""` and failed on exactly the fields it was
56
+ built for. It now re-resolves the field by id **or name** each poll and asserts the live
57
+ `.value` property (via `evaluate_script`), covering enabled AND disabled/computed fields
58
+ while keeping the morph-immunity. `have_reactive_text` (textContent) was unaffected.
59
+
50
60
  - **A freshly-rendered `reactive_compute` root now self-seeds its derived fields on connect (#199).**
51
61
  Before, a compute root computed nothing until the first user `input` — a fresh render
52
62
  (or a server validation-error re-render that replaced the body) left every derived
@@ -63,21 +63,24 @@ module Phlex
63
63
  end
64
64
 
65
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.
66
+ # RE-RESOLVING the field by its id on every poll and reading its live
67
+ # `.value` PROPERTY (issue #204) NOT the value attribute. A
68
+ # reactive_compute reducer paints a computed output with `el.value = …`
69
+ # (the property); for a DISABLED / read-only output the value attribute
70
+ # never reflects that, so an attribute-based matcher reads "" and fails.
71
+ # Reading the property covers enabled AND disabled/computed fields — the
72
+ # exact case this matcher was built for — and keeps the morph-immunity
73
+ # (each poll re-finds the node by id, so a re-seed/morph that replaces the
74
+ # input can't surface a stale node or a transient blank).
71
75
  #
72
76
  # expect(page).to have_reactive_value("total", "6")
73
77
  #
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
+ # `timeout:` (or `wait:`) overrides Capybara's default max wait. The
79
+ # `have_` prefix is Capybara-matcher convention (have_field/have_css), NOT
80
+ # a predicate hence the PredicatePrefix disable, mirroring matchers.rb.
78
81
  # rubocop:disable Naming/PredicatePrefix
79
- def have_reactive_value(id, value, **)
80
- have_field(id, with: value, **)
82
+ def have_reactive_value(id, value, timeout: nil, wait: nil)
83
+ ReactiveValueMatcher.new(id, value, wait: timeout || wait)
81
84
  end
82
85
 
83
86
  # Assert (waiting) that the node with DOM id `id` has TEXT `value`,
@@ -91,6 +94,91 @@ module Phlex
91
94
  end
92
95
  # rubocop:enable Naming/PredicatePrefix
93
96
 
97
+ # A waiting matcher that asserts a field's live `.value` PROPERTY (issue
98
+ # #204), read by id via evaluate_script so it sees a value a reducer set
99
+ # on a DISABLED output (where the value attribute stays blank). Polls until
100
+ # the property equals the expected value or the wait budget elapses,
101
+ # re-resolving the node by id every cycle (morph-immune). Supports negation
102
+ # (`not_to`) the Capybara way: the negative holds as soon as the property
103
+ # differs from the expected value (an absent field reads nil, which never
104
+ # equals a String expectation — so a missing field satisfies `not_to`).
105
+ #
106
+ # A plain class (not RSpec::Matchers.define) so it needs no RSpec at load
107
+ # time beyond the duck-typed matcher protocol RSpec calls (matches?,
108
+ # does_not_match?, failure_message*), keeping System loadable under Capybara
109
+ # alone. The expected value is stringified because a DOM `.value` is always
110
+ # a JS string on the wire (a numeric literal like 6 compares as "6").
111
+ class ReactiveValueMatcher
112
+ def initialize(id, value, wait: nil)
113
+ @id = id
114
+ @expected = value.to_s
115
+ @wait = wait
116
+ end
117
+
118
+ def matches?(page)
119
+ @page = page
120
+ poll_until { current_value == @expected }
121
+ end
122
+
123
+ # does_not_match? is RSpec's REQUIRED negated-matcher protocol method — its
124
+ # name is fixed by RSpec, not a predicate we get to rename.
125
+ # rubocop:disable Naming/PredicatePrefix
126
+ def does_not_match?(page)
127
+ @page = page
128
+ # The negative is satisfied the moment the property is NOT the expected
129
+ # value (or the field is absent → nil). poll_until returns true as soon
130
+ # as that holds, false if it stayed equal for the whole budget.
131
+ poll_until { current_value != @expected }
132
+ end
133
+ # rubocop:enable Naming/PredicatePrefix
134
+
135
+ def failure_message
136
+ "expected ##{@id} to have value #{@expected.inspect} (its .value property), " \
137
+ "but after waiting it was #{current_value.inspect}"
138
+ end
139
+
140
+ def failure_message_when_negated
141
+ "expected ##{@id} NOT to have value #{@expected.inspect} (its .value property), but it did"
142
+ end
143
+
144
+ private
145
+
146
+ # The field's live `.value` property, re-resolved each call. The
147
+ # identifier is matched by DOM id OR name (mirroring Capybara's
148
+ # have_field, which matches id/name/label) — reactive_field emits a
149
+ # `name`, not an `id`, so an id-only lookup would find nothing. Returns
150
+ # the String value, or nil when the field is absent (Playwright serializes
151
+ # a JS null as an empty Hash, so a non-String result is normalized to nil
152
+ # — the matcher then treats it as "not settled yet" and keeps polling
153
+ # rather than comparing a Hash to the expected String).
154
+ def current_value
155
+ raw = @page.evaluate_script(<<~JS)
156
+ (() => {
157
+ const k = #{@id.to_json}
158
+ const el = document.getElementById(k) || document.getElementsByName(k)[0]
159
+ return el ? el.value : null
160
+ })()
161
+ JS
162
+ raw.is_a?(::String) ? raw : nil
163
+ end
164
+
165
+ # Bounded poll on a JS-property condition Capybara can't express as a
166
+ # built-in waiting matcher. Uses the monotonic clock and the configured
167
+ # (or overridden) Capybara max wait — the same shape the system specs'
168
+ # hand-rolled wait_for used, now lifted into the helper.
169
+ def poll_until
170
+ deadline = now + (@wait || ::Capybara.default_max_wait_time)
171
+ loop do
172
+ return true if yield
173
+ return false if now >= deadline
174
+
175
+ sleep 0.05
176
+ end
177
+ end
178
+
179
+ def now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
180
+ end
181
+
94
182
  private
95
183
 
96
184
  # Fold an explicit `timeout:` into Capybara's `wait:` option, or omit it so
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.11.2"
5
+ VERSION = "0.11.3"
6
6
  end
7
7
  end
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.2
4
+ version: 0.11.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson