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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/phlex/reactive/test_helpers/system.rb +99 -11
- data/lib/phlex/reactive/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23b25b41281afc59503c63e9e4fbcee696d1265b96682c1c97bfd0adf385055d
|
|
4
|
+
data.tar.gz: 7204885d357720c85e57c8b44192d6e000f91c985346177a7ddcefc28f7451cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
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
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
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
|
-
|
|
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
|