phlex-reactive 0.11.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/app/javascript/phlex/reactive/reactive_controller.js +114 -7
- data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
- data/app/javascript/phlex/reactive/reactive_controller.min.js.map +3 -3
- data/lib/phlex/reactive/test_helpers/system.rb +105 -0
- data/lib/phlex/reactive/test_helpers.rb +6 -0
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +5 -0
- metadata +2 -1
|
@@ -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)
|
data/lib/phlex/reactive.rb
CHANGED
|
@@ -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.
|
|
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
|