capybara-storyboard 0.1.0

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.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Storyboard
5
+ module Policies
6
+ # Restricts screenshots to an explicit set of test files. The set holds
7
+ # already-normalized paths (see Capybara::Storyboard.normalize_test_path);
8
+ # #call normalizes the context's raw test_file the same way before matching.
9
+ #
10
+ # An empty set means "explicitly no targets" and always returns false. The
11
+ # backward-compatible "capture everything" behavior is handled upstream by
12
+ # default_policy, which simply never constructs this policy when no target
13
+ # list is configured.
14
+ class TargetListPolicy
15
+ def initialize(paths)
16
+ @paths = Set.new(paths)
17
+ end
18
+
19
+ def call(context)
20
+ return false if @paths.empty?
21
+
22
+ test_file = context&.test_file
23
+ return false if test_file.nil?
24
+
25
+ @paths.include?(Capybara::Storyboard.normalize_test_path(test_file))
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module Capybara
7
+ module Storyboard
8
+ # Per-example state (sequence counter, output directory, enabled flag,
9
+ # example metadata) plus filename / path / sanitize derivation.
10
+ #
11
+ # Extracting this state out of TestHelper keeps its unit tests independent
12
+ # of Capybara and RSpec hooks: a Session can be built with a plain example
13
+ # double and an injected +output_root+.
14
+ class Session
15
+ def initialize(example:, enabled:, output_root: nil)
16
+ @example = example
17
+ @enabled = enabled
18
+ @output_root = output_root
19
+ @index = 0
20
+ @dir = nil
21
+ @suppression_depth = 0
22
+ end
23
+
24
+ def enabled?
25
+ @enabled
26
+ end
27
+
28
+ # Automatic screenshot for a DSL action. No-op unless enabled.
29
+ def auto(page, action, detail = nil)
30
+ return unless @enabled
31
+ return if suppressed?
32
+
33
+ label = [action, sanitize(detail)].compact_blank.join('_')
34
+ capture_with_label(page, label)
35
+ end
36
+
37
+ # Manual screenshot hook. Like #auto, captured only when enabled. For an
38
+ # unconditional screenshot, use Capybara's own save_screenshot. The page
39
+ # is passed explicitly rather than held as state.
40
+ def manual(page, label)
41
+ return unless @enabled
42
+ return if suppressed?
43
+
44
+ capture_with_label(page, sanitize(label))
45
+ end
46
+
47
+ # Suppresses automatic/manual captures for the duration of the block. Used
48
+ # to skip nested captures while a confirm/alert dialog is open, where a
49
+ # screenshot or JS eval would raise UnexpectedAlertOpenError. A depth counter
50
+ # (not a boolean) supports nesting; the ensure guarantees reset on exceptions.
51
+ def suppress_captures
52
+ @suppression_depth += 1
53
+ yield
54
+ ensure
55
+ @suppression_depth -= 1
56
+ end
57
+
58
+ def suppressed?
59
+ @suppression_depth.positive?
60
+ end
61
+
62
+ private
63
+
64
+ def capture_with_label(page, label)
65
+ ensure_dir!
66
+ @index += 1
67
+ filename = "#{format('%03d', @index)}_#{label}.png"
68
+ wait_for_stable_page(page)
69
+ save_screenshot_safely(page, @dir.join(filename))
70
+ end
71
+
72
+ # A screenshot is a side effect; an unexpected failure (e.g. a dialog left
73
+ # open) must never break the test body. Mirrors PageStability's warn
74
+ # convention. There is no expected non-JS-driver case here, so warn always.
75
+ # (Named "safely" rather than +save_screenshot+ to avoid colliding with
76
+ # Capybara's DSL debugger method, which RuboCop's Lint/Debugger flags.)
77
+ def save_screenshot_safely(page, path)
78
+ page.save_screenshot(path)
79
+ rescue StandardError => e
80
+ warn("capybara-storyboard: screenshot skipped after error: #{e.class}: #{e.message}")
81
+ end
82
+
83
+ # Both callers (#auto / #manual) are already enabled-gated, so this always
84
+ # runs before an actual capture and never on a disabled session.
85
+ def wait_for_stable_page(page)
86
+ config = Capybara::Storyboard.configuration
87
+ PageStability.wait_for_stable_page(
88
+ page,
89
+ interval: config.page_stability_interval,
90
+ max_attempts: config.page_stability_max_attempts,
91
+ excluded_animations: config.page_stability_excluded_animations
92
+ )
93
+ end
94
+
95
+ def ensure_dir!
96
+ @dir ||= output_dir
97
+ FileUtils.mkdir_p(@dir)
98
+ end
99
+
100
+ def output_dir
101
+ (@output_root || default_output_root).join(spec_relative_dir, example_name)
102
+ end
103
+
104
+ # The output root when none is injected: the configuration's output_dir
105
+ # (default <base>/tmp/screenshots, overridable via Storyboard.configure).
106
+ def default_output_root
107
+ Capybara::Storyboard.configuration.output_dir
108
+ end
109
+
110
+ # Mirrors the spec file's location as the output directory tree, so
111
+ # screenshots for spec/system/foo_spec.rb land under system/foo/. The
112
+ # spec/ prefix and the _spec.rb (or .rb) suffix are dropped; each
113
+ # remaining path segment is sanitized while the / separators are kept.
114
+ def spec_relative_dir
115
+ raw = @example.metadata[:file_path]
116
+ return 'spec' if raw.blank?
117
+
118
+ relative = Capybara::Storyboard.normalize_test_path(raw)
119
+ segments = relative.split('/')
120
+ segments.shift if segments.first == 'spec'
121
+ segments[-1] = strip_spec_suffix(segments.last) if segments.any?
122
+
123
+ sanitized = segments.map { |segment| sanitize(segment) }.reject(&:blank?)
124
+ sanitized.empty? ? 'spec' : sanitized.join('/')
125
+ end
126
+
127
+ # Drops the trailing _spec.rb, falling back to a plain .rb, so a spec
128
+ # basename becomes its bare name (hoge_spec.rb -> hoge, hoge.rb -> hoge).
129
+ def strip_spec_suffix(basename)
130
+ basename.sub(/_spec\.rb\z/, '').sub(/\.rb\z/, '')
131
+ end
132
+
133
+ def example_name
134
+ name = sanitize(@example.description).truncate(80, omission: '')
135
+ return name if name.present?
136
+
137
+ fallback = sanitize(@example.full_description)
138
+ fallback.present? ? fallback.truncate(80, omission: '') : 'example'
139
+ end
140
+
141
+ # \p{Word} (Unicode word characters) rather than \w (ASCII-only) so that
142
+ # non-ASCII descriptions/labels (e.g. Japanese) are preserved in filenames
143
+ # and directory names instead of collapsing to underscores.
144
+ def sanitize(text)
145
+ text.to_s.gsub(/[^\p{Word}-]/, '_').gsub(/_+/, '_').gsub(/\A_|_\z/, '')
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Capybara
6
+ module Storyboard
7
+ # RSpec system-spec helper. Include AFTER Capybara::DSL so the overrides
8
+ # below chain into the DSL via +super+. A per-test policy call decides
9
+ # whether automatic screenshots are taken (see Capybara::Storyboard.policy).
10
+ module TestHelper
11
+ def self.included(base)
12
+ base.class_eval { before { __storyboard_init } }
13
+ end
14
+
15
+ # Manual screenshot hook. Captured only when enabled (like the automatic
16
+ # hooks); use Capybara's own save_screenshot for an unconditional shot.
17
+ def storyboard_screenshot(label)
18
+ @__storyboard.manual(page, label)
19
+ end
20
+
21
+ def visit(path, ...)
22
+ super.tap { @__storyboard.auto(page, 'visit', path) }
23
+ end
24
+
25
+ def click_on(locator = nil, ...)
26
+ @__storyboard.auto(page, 'before_click_on', locator)
27
+ super.tap { @__storyboard.auto(page, 'after_click_on', locator) }
28
+ end
29
+
30
+ def click_link(locator = nil, ...)
31
+ @__storyboard.auto(page, 'before_click_link', locator)
32
+ super.tap { @__storyboard.auto(page, 'after_click_link', locator) }
33
+ end
34
+
35
+ def click_button(locator = nil, ...)
36
+ @__storyboard.auto(page, 'before_click_button', locator)
37
+ super.tap { @__storyboard.auto(page, 'after_click_button', locator) }
38
+ end
39
+
40
+ def fill_in(locator, ...)
41
+ super.tap { @__storyboard.auto(page, 'fill_in', locator) }
42
+ end
43
+
44
+ def select(value = nil, ...)
45
+ super.tap { @__storyboard.auto(page, 'select', value) }
46
+ end
47
+
48
+ def check(locator, ...)
49
+ super.tap { @__storyboard.auto(page, 'check', locator) }
50
+ end
51
+
52
+ def uncheck(locator, ...)
53
+ super.tap { @__storyboard.auto(page, 'uncheck', locator) }
54
+ end
55
+
56
+ def choose(locator, ...)
57
+ super.tap { @__storyboard.auto(page, 'choose', locator) }
58
+ end
59
+
60
+ def attach_file(locator, ...)
61
+ super.tap { @__storyboard.auto(page, 'attach_file', locator) }
62
+ end
63
+
64
+ def accept_confirm(...)
65
+ @__storyboard.suppress_captures { super }.tap { @__storyboard.auto(page, 'accept_confirm') }
66
+ end
67
+
68
+ def accept_alert(...)
69
+ @__storyboard.suppress_captures { super }.tap { @__storyboard.auto(page, 'accept_alert') }
70
+ end
71
+
72
+ private
73
+
74
+ def __storyboard_init
75
+ example = RSpec.current_example
76
+ @__storyboard = Capybara::Storyboard::Session.new(
77
+ example:,
78
+ enabled: Capybara::Storyboard.policy.call(__storyboard_context(example))
79
+ )
80
+ end
81
+
82
+ # Derivation lives here so Context stays a plain value holder.
83
+ def __storyboard_context(example)
84
+ metadata = example.metadata
85
+ described = metadata[:described_class]
86
+ Capybara::Storyboard::Context.new(
87
+ test_class_name: described.respond_to?(:name) ? described.name : nil,
88
+ test_method_name: example.description,
89
+ test_file: metadata[:file_path]
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Storyboard
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ require_relative 'storyboard/version'
7
+
8
+ module Capybara
9
+ # Automatic Capybara screenshots for RSpec system specs, gated by a
10
+ # replaceable policy (see Capybara::Storyboard.policy).
11
+ module Storyboard
12
+ class Error < StandardError; end
13
+
14
+ class << self
15
+ # The single Configuration holding user overrides (output root, policy).
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ # Yields the Configuration for block-style setup:
21
+ # Capybara::Storyboard.configure { |config| config.output_dir = ... }
22
+ def configure
23
+ yield(configuration)
24
+ end
25
+
26
+ # Drops every override (output_dir and policy) in one shot, restoring the
27
+ # gem's defaults. Handy in `after` hooks so a customized run never leaks
28
+ # into later examples.
29
+ def reset_configuration!
30
+ @configuration = nil
31
+ end
32
+
33
+ # Test-hygiene helper mirroring reset_configuration! / reset_policy!:
34
+ # clears the run-once flag so clear_output! can be exercised again
35
+ # within the same process (a real run never needs this).
36
+ def reset_output_cleared!
37
+ @output_cleared = nil
38
+ end
39
+
40
+ # Empties the output root once per process so a run's screenshots never mix
41
+ # with stale files left by a previous run. Meant to be wired into a
42
+ # before(:suite) hook by the host app (see the README); not called
43
+ # automatically, so nothing is registered on RSpec just by requiring the gem.
44
+ def clear_output!
45
+ # Run-once guard: idempotent even if the before(:suite) hook fires more
46
+ # than once (e.g. one hook per RSpec process under parallel_tests).
47
+ return if @output_cleared
48
+
49
+ # Arm gate: same bare-SCREENSHOTS probe as default_policy's own gate
50
+ # (EnvPolicy ignores its argument, so call(nil) is context-free). We
51
+ # deliberately do NOT use configuration.policy here: a custom policy
52
+ # assumes a per-example Context and may read the target list (raising
53
+ # when SCREENSHOT_TESTS_FILE is bad). A suite-wide clear must key off
54
+ # the global arm state alone, never per-example data.
55
+ return unless Policies::EnvPolicy.new.call(nil)
56
+
57
+ # Honor the run-once contract BEFORE touching the filesystem: if the
58
+ # delete fails, we must not keep retrying (and re-deleting) on later
59
+ # registrations of the same hook.
60
+ @output_cleared = true
61
+
62
+ # Resolve the root against the shared base so a relative output_dir
63
+ # (kept relative by Configuration) maps to a single absolute target.
64
+ root = configuration.output_dir.expand_path(rails_root_or_pwd)
65
+ return unless safe_to_clear?(root)
66
+ # First run / nothing captured yet: silently skip. Session#ensure_dir!
67
+ # lazily recreates the tree on the first capture, so a missing root is
68
+ # not an error.
69
+ return unless root.exist?
70
+
71
+ FileUtils.rm_rf(root)
72
+ end
73
+
74
+ # An object responding to #call(context) -> Boolean.
75
+ # Assign nil (or call reset_policy!) to restore the default.
76
+ # Delegated to the Configuration so there is a single source of truth.
77
+ def policy
78
+ configuration.policy
79
+ end
80
+
81
+ def policy=(value)
82
+ configuration.policy = value
83
+ end
84
+
85
+ # Equivalent to `self.policy = nil`; named for readable `after` hooks that
86
+ # prevent a custom policy from leaking into later examples.
87
+ def reset_policy!
88
+ configuration.reset_policy!
89
+ end
90
+
91
+ # Normalizes a test file path to a base-relative string so that target
92
+ # list entries and Context#test_file compare equal regardless of leading
93
+ # `./`, absolute vs relative form, or surrounding whitespace/newlines.
94
+ # Shared by default_policy (list side) and TargetListPolicy#call
95
+ # (context side) so both sides always agree on the canonical form.
96
+ def normalize_test_path(path)
97
+ base = rails_root_or_pwd
98
+ Pathname(path.to_s.strip)
99
+ .expand_path(base)
100
+ .relative_path_from(base)
101
+ .to_s
102
+ end
103
+
104
+ # The single place that resolves the base directory: Rails.root when
105
+ # available (the gem stays Rails-optional), else the current directory.
106
+ # Session#default_output_root joins its own subpath onto this.
107
+ def rails_root_or_pwd
108
+ if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
109
+ Rails.root
110
+ else
111
+ Pathname(Dir.pwd)
112
+ end
113
+ end
114
+
115
+ # The single place that builds the default policy. Internal API, public
116
+ # only so Configuration#policy can call it without `send`; use `policy`.
117
+ #
118
+ # When SCREENSHOTS is unset the mechanism is disarmed: return EnvPolicy
119
+ # alone WITHOUT touching the target list. This honors the "disabled ->
120
+ # nothing happens" contract even when SCREENSHOT_TESTS_FILE points at a
121
+ # missing file (its existence check must not blow up a disarmed suite).
122
+ #
123
+ # When armed: no target list configured (neither ENV set) -> EnvPolicy
124
+ # alone, preserving the gist's "SCREENSHOTS=1 captures everything". A
125
+ # target list configured (even an empty file) -> EnvPolicy AND
126
+ # TargetListPolicy; an empty set then means "explicitly zero targets".
127
+ def default_policy
128
+ env = Policies::EnvPolicy.new
129
+ # env.call(nil) probes the bare SCREENSHOTS switch (same gate
130
+ # clear_output! uses), inlined here so we can reuse this exact `env`
131
+ # instance as the return value below instead of building a second one.
132
+ # Disarmed -> skip reading/validating the target list entirely.
133
+ return env unless env.call(nil)
134
+
135
+ raw_targets = raw_target_list
136
+ return env if raw_targets.nil?
137
+
138
+ # Drop blank entries BEFORE normalizing: an empty string normalizes to
139
+ # "." (the base dir), which is not blank and would leak into the set.
140
+ targets = raw_targets.map { |path| path.to_s.strip }.compact_blank.map { |path| normalize_test_path(path) }
141
+ target = Policies::TargetListPolicy.new(targets)
142
+
143
+ # Composition style: a plain lambda. A proc responds to #call, so it
144
+ # satisfies the #call(context) -> Boolean policy contract, and it can
145
+ # still be replaced wholesale via #policy=.
146
+ ->(context) { env.call(context) && target.call(context) }
147
+ end
148
+
149
+ private
150
+
151
+ # Minimal foot-gun guard for the suite-wide clear: refuse the obviously
152
+ # dangerous roots (a filesystem root, or the project/cwd root itself)
153
+ # rather than rm_rf-ing them. Mirrors the "safe no-op + warn" convention
154
+ # used elsewhere (see Session#save_screenshot_safely); we deliberately
155
+ # stop at these two cases rather than attempting broad path sanitizing.
156
+ def safe_to_clear?(root)
157
+ if root.parent == root || root == rails_root_or_pwd
158
+ warn("capybara-storyboard: refusing to clear unsafe output root: #{root}")
159
+ return false
160
+ end
161
+
162
+ true
163
+ end
164
+
165
+ # Returns the raw (un-normalized) target list, or nil when no target list
166
+ # is configured at all. SCREENSHOT_TESTS_FILE and SCREENSHOT_TESTS are
167
+ # unioned when both are present.
168
+ def raw_target_list
169
+ file_entries = raw_target_list_from_file
170
+ inline_entries = raw_target_list_inline
171
+ return nil if file_entries.nil? && inline_entries.nil?
172
+
173
+ (file_entries || []) + (inline_entries || [])
174
+ end
175
+
176
+ # Reads SCREENSHOT_TESTS_FILE. Returns nil when unset/blank, raises when
177
+ # the path is set but missing (a silent all/none capture would be worse
178
+ # than a loud failure), otherwise the newline-split contents.
179
+ def raw_target_list_from_file
180
+ path = ENV.fetch('SCREENSHOT_TESTS_FILE', nil)
181
+ return nil unless path.present?
182
+
183
+ raise Error, "SCREENSHOT_TESTS_FILE does not exist: #{path}" unless File.exist?(path)
184
+
185
+ File.read(path).split("\n")
186
+ end
187
+
188
+ # Reads SCREENSHOT_TESTS. Returns nil when unset/blank, otherwise the
189
+ # comma-split entries.
190
+ def raw_target_list_inline
191
+ value = ENV.fetch('SCREENSHOT_TESTS', nil)
192
+ return nil unless value.present?
193
+
194
+ value.split(',')
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ require_relative 'storyboard/context'
201
+ require_relative 'storyboard/policies/env_policy'
202
+ require_relative 'storyboard/policies/target_list_policy'
203
+ require_relative 'storyboard/configuration'
204
+ require_relative 'storyboard/page_stability'
205
+ require_relative 'storyboard/session'
206
+ require_relative 'storyboard/test_helper'
@@ -0,0 +1,6 @@
1
+ module Capybara
2
+ module Storyboard
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: capybara-storyboard
3
+ description: Visually verify UI changes by running system specs with capybara-storyboard and reading the captured screenshot sequence. Use whenever app/views or app/components changes need a visual check, or when a system spec is changed and run.
4
+ model: sonnet
5
+ ---
6
+
7
+ # Capybara Storyboard visual review
8
+
9
+ Capybara::Storyboard captures a Capybara operation (`visit`, `click_on`, ...) together with a
10
+ screenshot at each step of a system spec, and lays the screenshots out as an ordered sequence
11
+ per test. This skill drives an agent through generating and reading that sequence to catch
12
+ things a passing test can't: layout breakage, leftover UI state (a modal that didn't close, a
13
+ flash message still showing), error screens, or anything else that looks wrong for the action
14
+ and point in the test where it occurs.
15
+
16
+ Reviewing screenshots one image at a time only works if the set stays small — narrow to the
17
+ system specs actually touched by the change, not the whole suite. Capturing every system spec
18
+ would produce hundreds of images, which defeats the purpose.
19
+
20
+ ## When this applies
21
+
22
+ - `app/views` or `app/components` were changed and the resulting screen needs a visual check.
23
+ - A system spec was changed and is being run.
24
+
25
+ ## Steps
26
+
27
+ ### 1. Determine the target specs
28
+
29
+ Narrow to the system specs relevant to the change:
30
+
31
+ - If `app/views` or `app/components` changed, identify which system specs exercise the changed
32
+ screen(s) — search for the view/component name, the controller action, or the route it
33
+ belongs to. Use this bullet whenever the call sites are countable: if you can enumerate all
34
+ the places the changed view/component is used and there are roughly 10 or fewer, target those
35
+ specific call sites directly — even if the changed file is itself called a shared "component".
36
+ If more than one spec is a plausible match and it isn't obvious which apply, ask the user.
37
+ - If enumerating every call site is impractical, or the changed file is a layout or
38
+ partial/component rendered by most/all pages rather than a handful of named places (so the
39
+ search above would return most or all system specs, not a short list), don't try to enumerate
40
+ every caller. Instead propose a small representative sample (roughly 3-6 specs) spanning
41
+ distinct usage contexts where the change is most likely to visually clash — e.g. a signed-out
42
+ page, a standard authenticated page, an admin/dense-UI page, a mobile/narrow-viewport spec if
43
+ one exists — and confirm the sample with the user before running, since you can't be sure
44
+ those spec files exist or still match without checking.
45
+ - If a system spec itself was changed, that spec is the target.
46
+
47
+ ### 2. Run the specs and capture screenshots
48
+
49
+ Run the target specs locally with only `SCREENSHOTS=1` set, passing the target spec files
50
+ directly as `rspec` arguments so only the relevant tests produce screenshots:
51
+
52
+ ```bash
53
+ SCREENSHOTS=1 bundle exec rspec spec/system/login_spec.rb spec/system/signup_spec.rb
54
+ ```
55
+
56
+ Running `bundle exec rspec` with `SCREENSHOTS=1` and no spec path arguments captures **every**
57
+ system spec — avoid this unless the user explicitly wants a full-suite review, since it
58
+ produces an impractically large set of images to read one at a time.
59
+
60
+ ### 3. Enumerate the output
61
+
62
+ Screenshots land at:
63
+
64
+ ```
65
+ tmp/screenshots/{GroupName}/{example_name}/{NNN_action_detail}.png
66
+ ```
67
+
68
+ Each `{GroupName}/{example_name}` directory is one system spec example — treat it as one unit
69
+ of review. Within a directory, list PNGs in ascending `NNN` order: that zero-padded prefix is
70
+ the order the actions happened during the test, and the rest of the filename names the action
71
+ (e.g. `001_visit_users.png`, `002_before_click_on_Done.png`,
72
+ `003_after_click_on_Done.png`). Click methods (`click_on`, `click_link`, `click_button`)
73
+ produce a before/after pair; everything else produces one screenshot taken after the action.
74
+
75
+ ### 4. Read each image in sequence
76
+
77
+ For each `{GroupName}/{example_name}` directory, read its PNGs in `NNN` order, one at a time.
78
+ For each image, use the filename (the action) and its position in the sequence (what happened
79
+ right before it) to judge whether the screen looks right at that point — check for layout
80
+ breakage, unintended or leftover UI state, error pages, and anything else inconsistent with
81
+ the action just performed.
82
+
83
+ ### 5. Report per test
84
+
85
+ For each `{GroupName}/{example_name}` directory report:
86
+
87
+ - Which test it is.
88
+ - For any issue found: which action/sequence number (by filename) it appeared right after,
89
+ and what was observed.
90
+
91
+ A directory with no issues just needs a short confirmation — don't pad the report.
92
+
93
+ ### 6. Clean up
94
+
95
+ Remove the local `tmp/screenshots/**` output once the review is done, unless the user wants it
96
+ kept for later reference.
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capybara-storyboard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - aki
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: capybara
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Capybara::Storyboard records Capybara system test operations and their
27
+ screenshots, then visualizes them as a storyboard so you can review test flows at
28
+ a glance.
29
+ email:
30
+ - lala.akira@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - CODE_OF_CONDUCT.md
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - docs/github-actions.md
41
+ - lib/capybara/storyboard.rb
42
+ - lib/capybara/storyboard/configuration.rb
43
+ - lib/capybara/storyboard/context.rb
44
+ - lib/capybara/storyboard/page_stability.rb
45
+ - lib/capybara/storyboard/policies/env_policy.rb
46
+ - lib/capybara/storyboard/policies/target_list_policy.rb
47
+ - lib/capybara/storyboard/session.rb
48
+ - lib/capybara/storyboard/test_helper.rb
49
+ - lib/capybara/storyboard/version.rb
50
+ - sig/capybara/storyboard.rbs
51
+ - skills/capybara-storyboard/SKILL.md
52
+ homepage: https://github.com/aki77/capybara-storyboard
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/aki77/capybara-storyboard
57
+ source_code_uri: https://github.com/aki77/capybara-storyboard/tree/main
58
+ changelog_uri: https://github.com/aki77/capybara-storyboard/blob/main/CHANGELOG.md
59
+ rubygems_mfa_required: 'true'
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.4.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 4.0.15
75
+ specification_version: 4
76
+ summary: Capture and visualize Capybara system test flows as screenshot storyboards.
77
+ test_files: []