view_spec 0.0.0 → 0.0.1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +267 -24
  4. data/app/controllers/concerns/view_specs/preview_actions.rb +37 -0
  5. data/app/controllers/view_spec_previews_controller.rb +3 -0
  6. data/app/views/view_specs/_group.html.erb +56 -0
  7. data/app/views/view_specs/_scenario.html.erb +55 -0
  8. data/app/views/view_specs/_spec.html.erb +18 -0
  9. data/app/views/view_specs/group/_notes.html.erb +14 -0
  10. data/app/views/view_specs/group/_preview.html.erb +19 -0
  11. data/app/views/view_specs/group/_raw.html.erb +7 -0
  12. data/app/views/view_specs/group/_source.html.erb +7 -0
  13. data/app/views/view_specs/previews/index.html.erb +22 -0
  14. data/lib/view_spec/collection.rb +43 -0
  15. data/lib/view_spec/config.rb +26 -3
  16. data/lib/view_spec/context.rb +40 -0
  17. data/lib/view_spec/dsl/controller.rb +7 -2
  18. data/lib/view_spec/dsl/groups.rb +2 -2
  19. data/lib/view_spec/dsl/layout.rb +7 -1
  20. data/lib/view_spec/dsl/notes.rb +22 -0
  21. data/lib/view_spec/dsl/params.rb +25 -15
  22. data/lib/view_spec/dsl/preview.rb +25 -0
  23. data/lib/view_spec/dsl/scenarios.rb +6 -6
  24. data/lib/view_spec/dsl/tests.rb +18 -0
  25. data/lib/view_spec/dsl/title.rb +1 -1
  26. data/lib/view_spec/engine.rb +20 -1
  27. data/lib/view_spec/entry.rb +62 -0
  28. data/lib/view_spec/entry_collection.rb +11 -0
  29. data/lib/view_spec/error.rb +4 -0
  30. data/lib/view_spec/executable_proc.rb +32 -0
  31. data/lib/view_spec/executable_string.rb +23 -0
  32. data/lib/view_spec/group.rb +17 -13
  33. data/lib/view_spec/group_collection.rb +4 -0
  34. data/lib/view_spec/group_context.rb +10 -0
  35. data/lib/view_spec/group_preview.rb +52 -0
  36. data/lib/view_spec/markdown.rb +20 -0
  37. data/lib/view_spec/markdown_renderer.rb +13 -0
  38. data/lib/view_spec/minitest/tests.rb +27 -0
  39. data/lib/view_spec/note.rb +44 -0
  40. data/lib/view_spec/param.rb +12 -2
  41. data/lib/view_spec/param_set.rb +17 -11
  42. data/lib/view_spec/preview.rb +44 -0
  43. data/lib/view_spec/registry.rb +7 -23
  44. data/lib/view_spec/renderable.rb +8 -7
  45. data/lib/view_spec/scenario.rb +5 -49
  46. data/lib/view_spec/scenario_collection.rb +4 -0
  47. data/lib/view_spec/scenario_context.rb +11 -0
  48. data/lib/view_spec/scenario_preview.rb +30 -0
  49. data/lib/view_spec/source_file.rb +22 -0
  50. data/lib/view_spec/spec.rb +3 -13
  51. data/lib/view_spec/spec_collection.rb +7 -0
  52. data/lib/view_spec/spec_context.rb +11 -0
  53. data/lib/view_spec/test_case.rb +10 -0
  54. data/lib/view_spec/test_helpers.rb +23 -0
  55. data/lib/view_spec/types/symbol.rb +1 -1
  56. data/lib/view_spec/utils.rb +42 -1
  57. data/lib/view_spec/version.rb +1 -1
  58. data/lib/view_spec.rb +16 -2
  59. metadata +67 -15
  60. data/app/views/view_spec/_group.html.erb +0 -20
  61. data/app/views/view_spec/_scenario.html.erb +0 -17
  62. data/app/views/view_spec/_spec.html.erb +0 -10
  63. data/lib/view_spec/dsl/context.rb +0 -54
  64. data/lib/view_spec/dsl.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db2b2ddc540389235a0c75db6bf93c85e65b854574623ea09b782e24d330d6ad
4
- data.tar.gz: f7c9400e0487de9c32c2cd0fa345c4c29e6bdceae3143bdb310c0bd2568c0390
3
+ metadata.gz: b0f44c0f0dbadabf2479727d19e5744a33c1d3e029ae7a3ea2d2c2d0a47bfe9f
4
+ data.tar.gz: 4f78e8fd46c7780e7816358375a0d67ae46e18c7858e67dd1871d7db1349c51c
5
5
  SHA512:
6
- metadata.gz: e8fe0a339f4f6e8b22c212f5f21d70c66bdedf8e1ffb1c56b03ce236025185a2b28a0cce1510fec59ceeab038550689016d998dce823af51d30b61d83c68f3f1
7
- data.tar.gz: 6d720f348d61915a323f0217d30af8fa09cb633918a13d4480e65db52c1433dc37b552b5e3abc407eff994e464f5212e90a8d990389d34fb185fe0acea3ab315
6
+ metadata.gz: 46dab0defa019b13aa07e9ea04392f810b4673b3a79379bff869d32d3adfeb0a93bd3da59397b2c9c49e8a64eb2d89cfbfa7911cb94a4ae56d7f462460568137
7
+ data.tar.gz: 2a165392e7a6864738aa841f0926164ad4aa701f0bc9acaad3baaf89d9df33f9b3b995077382a9ca7804ebdf6aedae4a038419937496e430c4fc6995006dabeb
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021-present Mark Perkins
3
+ Copyright (c) 2025-present Mark Perkins
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,51 +1,294 @@
1
- # ViewSpec
1
+ # ViewSpec
2
2
 
3
- ViewSpec lets you create example-led specifications for components, partials and views using an expressive Ruby DSL.
3
+ <p><a href="https://rubygems.org/gems/view_spec"><img src="https://img.shields.io/gem/v/view_spec" alt="Gem version"></a>
4
+ <a href="https://github.com/lookbook/view_spec/actions/workflows/ci.yml"><img src="https://github.com/lookbook/view_spec/actions/workflows/ci.yml/badge.svg" alt="CI status"></a></p>
4
5
 
5
- Specs can then be used in your tests, to render previews for development and visual testing or to generate documentation.
6
+ ViewSpec helps you **develop**, **test** and **document** the view layer of your Rails app using **example-led specifications** written in an **expressive Ruby DSL**. Specs can be created for your app's **components**, **partials**, **helpers**, **views** and more.
7
+
8
+ Each spec is made up of one or more **scenarios**, each of which represents a particular state or variant of the the subject of the spec. You can define a **preview**, add contextual **tests** and include **usage information** for each scenario.
9
+
10
+ ViewSpec then handles generating **live preview endpoints** and **integrating the tests into your test suite**, and makes it easy to **generate developer documentation** from the specs you create.
11
+
12
+ > [!IMPORTANT]
13
+ > **ViewSpec is at a very early stage of development.** Bugs are very likely and breaking changes may happen without warning.
14
+ > Please give it a try and share any thoughts, feedback or comments via issues/discussions but be aware that there will likely be significant changes to ViewSpec's functionality and API before a stable `v1.0` release candidate is reached.
15
+
16
+ ## ViewSpec files and DSL
17
+
18
+ ViewSpec spec files are written in Ruby and should be placed in the `test/view_specs` directory. Their filenames should end in `_vspec.rb` and can be organised in whatever subfolder struture is appropriate for your needs.
19
+
20
+ ViewSpec specs are written in a bespoke Ruby DSL that should feel somewhat familiar if you have used [RSpec](https://github.com/rspec/rspec) before.
21
+
22
+ Here's what an example spec file for a button component might look like:
6
23
 
7
24
  ```rb
8
- # button_vspec.rb
25
+ # test/view_specs/button_vspec.rb
9
26
 
10
27
  ViewSpec.spec ButtonComponent do
11
- title "Button (ViewComponent)"
12
-
13
28
  scenario "default" do
29
+ notes <<~MD
30
+ Default button with the `primary` theme.
31
+ _Appropriate for the majority of use-cases._
32
+ MD
33
+
14
34
  preview do
15
35
  render ButtonComponent.new(text: "Button text")
16
36
  end
37
+
38
+ tests do
39
+ test "it renders" do
40
+ render_preview
41
+ assert_selector("button")
42
+ end
43
+
44
+ test "it displays the label text" do
45
+ render_preview
46
+ assert_selector("button", text: "Button text")
47
+ end
48
+ end
17
49
  end
18
50
 
19
- scenario "editable params" do
51
+ scenario "themes" do
52
+ title "Available Themes"
53
+
54
+ notes <<~MD
55
+ There are three button themes available for use:
56
+
57
+ * `:primary` (default)
58
+ * `:secondary` (use for less important actions)
59
+ * `:danger` (use for destructive actions)
60
+ MD
61
+
62
+ preview <<~ERB
63
+ <%= render ButtonComponent.new(text: "Primary", theme: :primary) %>
64
+ <%= render ButtonComponent.new(text: "Secondary", theme: :secondary) %>
65
+ <%= render ButtonComponent.new(text: "Danger", theme: :danger) %>
66
+ ERB
67
+
68
+ tests do
69
+ test "it applies themes to the buttons" do
70
+ render_preview
71
+ assert_selector(%(button[data-theme="primary"]))
72
+ assert_selector(%(button[data-theme="secondary"]))
73
+ assert_selector(%(button[data-theme="danger"]))
74
+ end
75
+ end
76
+ end
77
+
78
+ scenario "playground" do
79
+ notes <<~MD
80
+ You can append query parameters to the preview URL to
81
+ set the button's `text`, `type` and `theme` properties.
82
+ MD
83
+
20
84
  param :text, :string, default: "Button text"
21
85
  param :type, :symbol, default: :button
86
+ param :theme, :symbol, default: :primary
22
87
 
23
88
  preview do
24
89
  render ButtonComponent.new(**params)
25
90
  end
26
91
  end
27
-
28
- scenario "buttons galore" do
29
- preview <<~ERB
30
- <%= render ButtonComponent.new(text: "Button 1") %>
31
- <%= render ButtonComponent.new(text: "Button 2") %>
32
- <%= render ButtonComponent.new(text: "Button 3") %>
33
- <%= render ButtonComponent.new(text: "Button 4") %>
34
- ERB
35
- end
92
+ end
93
+ ```
94
+
95
+ > [!NOTE]
96
+ > ViewSpec is completely agnostic to your choice of component framework (e.g. ViewComponent, Phlex, etc), templating language (ERB, HAML, etc) and test suite.
97
+ > If you can use it with Rails you can use it with ViewSpec!
98
+
99
+ ## Workflow example
36
100
 
37
- group "themes" do
38
- scenario "primary" do
39
- preview { render ButtonComponent.new(text: "Primary", theme: :primary) }
101
+ Below is an outline of what a ViewSpec-based workflow for developing, testing and documenting a new `button` component might look like.
102
+
103
+ The button component in this example will be implemented using a standard Rails/ActionView partial, but could equally be created using ViewComponent, Phlex, a view helper or even just vanilla HTML.
104
+
105
+ We'll assume that the ViewSpec gem has already been installed and configured in the example app.
106
+
107
+ ### 1. Create the component
108
+
109
+ First we create the basic `button` component partial in the `app/views/components` directory.
110
+
111
+ ```erb
112
+ <%# locals: (text:, theme: :primary, type: :button) %>
113
+
114
+ <%= button_tag(text, type: type, data: {theme: theme}, class: "button") %>
115
+ ```
116
+
117
+ From the file above you can see that there are default values for the `theme` and `type` options. Only the button `text` argument is required to render the partial.
118
+
119
+ ### 2. Add a spec file
120
+
121
+ Now we'll start by creating a bare-bones button spec at `test/view_specs/button_vspec.rb` that includes a single scenario describing the default state of the button.
122
+
123
+ ```rb
124
+ # test/view_specs/button_vspec.rb
125
+
126
+ ViewSpec.spec "components/button" do
127
+ title "Basic button"
128
+
129
+ scenario "default" do
130
+ notes <<~MD
131
+ Default button with `primary` theme.
132
+ _Appropriate for the majority of use-cases._
133
+ MD
134
+
135
+ preview do
136
+ render "components/button", text: "Click here"
40
137
  end
138
+ end
139
+ end
140
+ ```
141
+
142
+ ### 3. Inspect the preview
143
+
144
+ The default button preview defined above will now be available for viewing at [`https://localhost:3000/rails/view_spec/previews/button@default`](https://localhost:3000/rails/view_spec/previews/button@default).
145
+
146
+ Previews are rendered in isolation from the rest of your app which makes them perfect for using whilst developing your components/partials/helpers etc.
147
+
148
+ > [!NOTE]
149
+ > By default previews are only available when your app is running in `development` and `test` environments, although this behaviour is configurable if required.
150
+
151
+ ### 4. Add some tests
152
+
153
+ Once a scenario preview has been added you can then add some tests to run against it.
154
+
155
+ Tests are defined within the context of the scenario in a `tests do ... end` block.
156
+
157
+ For this example we'll add a couple of test to check that the rendered output of this default button component state is as expected.
158
+
159
+ ```rb
160
+ # test/view_specs/button_vspec.rb
161
+
162
+ ViewSpec.spec "components/button" do
163
+ # ...
164
+
165
+ scenario "default" do
166
+ # ...
41
167
 
42
- scenario "secondary" do
43
- preview { render ButtonComponent.new(text: "Secondary", theme: :secondary) }
168
+ preview do
169
+ render "components/button", text: "Click here"
44
170
  end
45
171
 
46
- scenario "danger" do
47
- preview { render ButtonComponent.new(text: "Danger", theme: :danger) }
172
+ tests do
173
+ setup { render_preview }
174
+
175
+ test "it renders the button component" do
176
+ assert_selector("button.button")
177
+ end
178
+
179
+ test "it has the default attributes set" do
180
+ assert_selector(%(button[type="button"][data-theme="primary"]))
181
+ end
182
+
183
+ test "it displays the label text" do
184
+ assert_selector("button", text: "Click here")
185
+ end
48
186
  end
49
187
  end
50
188
  end
51
- ```
189
+ ```
190
+
191
+ ### 5. Run the tests
192
+
193
+ To run the tests as part of your Minitest-based test suite you will first need to add a ViewSpec test file into your `tests` directory. It should contain the following content:
194
+
195
+ ```rb
196
+ # tests/view_specs_test.rb
197
+
198
+ require "test_helper"
199
+
200
+ module ViewSpecsTest
201
+ include ViewSpec::Minitest::Tests
202
+ end
203
+ ```
204
+
205
+ This only needs to be done once. With with this file in place all the preview tests defined in your spec files will be included everytime your test suite is run using `rails test`.
206
+
207
+ If you only wish to run your ViewSpec tests then just run the ViewSpec test file directly:
208
+
209
+ ```sh
210
+ rails test test/view_specs_test.rb
211
+ ```
212
+
213
+ > [!NOTE]
214
+ > Only Minitest is currently supported but adapters for RSpec and more are in the works!
215
+
216
+ ### 6. Add more scenarios
217
+
218
+ At this point it often makes sense to add more scenarios to demonstrate/document other states or properties of the subject of the spec.
219
+
220
+ A spec can have as many scenarios as needed, each with it's own preview, tests and usage notes.
221
+
222
+ > [!TIP]
223
+ > Scenarios can also be grouped together in a `group do ... end` block if required. See below for more details on scenarios and scenario groups.
224
+
225
+ ### 7. Generate documentation
226
+
227
+ ViewSpec files are designed to act as readable developer-documentation in their own right. However you may also wish to have HTML-formatted documentation for your components/partials/views etc.
228
+
229
+ ViewSpec provides some bare-bones default templates to make it easy to generate HTML docs from your specs.
230
+
231
+ For simplicity the example below just shows how you could generate docs specifically for the single button spec defined above (although in reality you would probably want to make this dynamic so you wouldn't need to manually add a separate action for each spec file).
232
+
233
+ ```rb
234
+ class DocsController < ApplicationController
235
+ def button
236
+ @spec = ViewSpec.specs.find(ButtonComponent)
237
+ @params = request.query_parameters
238
+ end
239
+ end
240
+ ```
241
+
242
+ ```erb
243
+ # views/docs/button.html.erb
244
+
245
+ <%= render @spec, params: @params %>
246
+ ```
247
+
248
+ The example above will render documentation for the button spec using [the default templates](./app/views/view_specs/).
249
+
250
+ Styling the docs with CSS is up to you, no default stylesheet is provided. With some basic styles applied the default templates look like this:
251
+
252
+ <img src="./.github/assets/button_view_spec_docs.png">
253
+
254
+ You can override one or all of the default templates by copying them into a `view_specs` folder in your `app/views` directory, and then customising them as needed.
255
+
256
+ ## Usage
257
+
258
+ ViewSpec has the following requirements:
259
+
260
+ * `Rails 7.0+`
261
+ * `Ruby 3.1+`
262
+
263
+ ### Installation
264
+
265
+ Add the following to your Rails app Gemfile:
266
+
267
+ ```rb
268
+ gem "view_spec"
269
+ ```
270
+
271
+ And then run `bundle install`. You are good to go!
272
+
273
+ ### Configuration
274
+
275
+ _Docs coming soon_
276
+
277
+ ### ViewSpec DSL
278
+
279
+ _Docs coming soon_
280
+
281
+ ## About ViewSpec (& Lookbook)
282
+
283
+ ViewSpec is being developed as a standalone replacement for [Lookbook's](https://lookbook.build/) underlying [preview system](https://lookbook.build/guide/previews).
284
+
285
+ It shares a lot of concepts with the existing Lookbook preview class format (which was based on ViewComponent's preview system - itself in turn inspired by Rails' ActionMailer previews). However using a pure Ruby DSL in place of YARD-style comment tags results in much better performance and has many other benefits (including 'native' syntax error handling and more).
286
+
287
+ Additionally, having a standalone file format and preview system which is not tied to any particular UI tool (such as Lookbook) or component system (such as ViewComponent) hopefully means that ViewSpec can be adopted by teams that don't want those dependencies but do still want a way to efficiently develop and document the frontend of their Rails app.
288
+
289
+ **The plan is for the next major version of Lookbook to built upon the ViewSpec file format**. It will provide a user interface for browsing and inspecting your ViewSpec previews in much the same way it does at the moment for the current (ViewComponent-style) preview class files.
290
+
291
+ _(Note that the current `v2.x` branch of Lookbook is **not** compatible with ViewSpec and there is no intention to add support for it in the future.)_
292
+
293
+ > [!IMPORTANT]
294
+ > Projects can (and always will be able to) use ViewSpec to help preview, test and document their view layer **without any requirement** to use Lookbook.
@@ -0,0 +1,37 @@
1
+ module ViewSpecs
2
+ module PreviewActions
3
+ extend ActiveSupport::Concern
4
+
5
+ def index
6
+ @view_specs = ViewSpec.specs
7
+
8
+ layout = self.class._layout || ViewSpec.config.preview_layout
9
+ options = {}
10
+ options[:layout] = layout if layout
11
+
12
+ render "view_specs/previews/index", **options
13
+ end
14
+
15
+ def preview
16
+ spec = ViewSpec.specs.find_where(lookup_path: params[:spec])
17
+ raise ActionController::RoutingError, "Spec not found" unless spec
18
+
19
+ entry = spec.entries.find_where(short_identifier: params[:entry])
20
+ raise ActionController::RoutingError, "`#{params[:entry]}` not found for spec `#{spec.subject}`" unless entry
21
+
22
+ preview = entry.preview(params: view_spec_preview_params)
23
+
24
+ options = {}
25
+ layout = entry.determine_layout(self.class._layout) || ViewSpec.config.preview_layout
26
+ (options[:layout] = layout) if layout
27
+
28
+ render preview, **options
29
+ end
30
+
31
+ private
32
+
33
+ def view_spec_preview_params
34
+ request.query_parameters.fetch(:params, {})
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ class ViewSpecPreviewsController < ApplicationController
2
+ include ViewSpecs::PreviewActions
3
+ end
@@ -0,0 +1,56 @@
1
+ <%# locals: (group:, params: {}, details: true) -%>
2
+
3
+ <%
4
+ details = (details == true) ? [ :notes, :preview, :source, :html] : Array.wrap(details)
5
+ previewable_scenarios = group.scenarios.filter(&:preview?)
6
+ request_params = request.query_parameters.fetch("params", {})
7
+ %>
8
+
9
+ <section class="vs-entry vs-entry--group">
10
+ <header class="vs-entry-header">
11
+ <h2 class="vs-entry-title"><%= group.title %></h2>
12
+ </header>
13
+ <div class="vs-entry-content">
14
+ <% if details.include?(:notes) && group.notes? %>
15
+ <article class="vs-entry-notes">
16
+ <%= group.notes %>
17
+ </article>
18
+ <% end %>
19
+ <% if previewable_scenarios.any? %>
20
+ <% preview = group.preview(params:) %>
21
+ <% if details.include?(:preview) %>
22
+ <details class="vs-entry-details vs-entry-details--preview" open>
23
+ <summary>
24
+ <h4 class="vs-entry-subtitle">Preview</h4>
25
+ </summary>
26
+ <p class="vs-entry-preview-link">
27
+ <%= link_to "View in new window", preview.to_path(params: {params: request_params}), target: "_blank" %>
28
+ </p>
29
+ <div class="vs-entry-details-display vs-preview">
30
+ <%= preview %>
31
+ </div>
32
+ </details>
33
+ <% end %>
34
+ <% if details.include?(:source) %>
35
+ <details class="vs-entry-details vs-entry-details--source" open>
36
+ <summary>
37
+ <h4 class="vs-entry-subtitle">Source</h4>
38
+ </summary>
39
+ <code class="vs-code-block" data-lang="<%= preview.lang %>">
40
+ <pre><%= preview.source %></pre>
41
+ </code>
42
+ </details>
43
+ <% end %>
44
+ <% if details.include?(:html) %>
45
+ <details class="vs-entry-details vs-entry-details--html">
46
+ <summary>
47
+ <h4 class="vs-entry-subtitle">Output</h4>
48
+ </summary>
49
+ <code class="vs-code-block" data-lang="html">
50
+ <pre><%= preview.raw %></pre>
51
+ </code>
52
+ </details>
53
+ <% end %>
54
+ <% end %>
55
+ </div>
56
+ </section>
@@ -0,0 +1,55 @@
1
+ <%# locals: (scenario:, params: {}, details: true) -%>
2
+
3
+ <%
4
+ details = (details == true) ? [ :notes, :preview, :source, :html] : Array.wrap(details)
5
+ request_params = request.query_parameters.fetch("params", {})
6
+ %>
7
+
8
+ <section class="vs-entry vs-entry--scenario">
9
+ <header class="vs-entry-header">
10
+ <h3 class="vs-entry-title"><%= scenario.title %></h3>
11
+ </header>
12
+ <div class="vs-entry-content">
13
+ <% if details.include?(:notes) && scenario.notes? %>
14
+ <article class="vs-entry-notes">
15
+ <%= scenario.notes %>
16
+ </article>
17
+ <% end %>
18
+ <% if scenario.preview? %>
19
+ <% preview = scenario.preview(params:) %>
20
+ <% if details.include?(:preview) %>
21
+ <details class="vs-entry-details vs-entry-details--preview" open>
22
+ <summary>
23
+ <h4 class="vs-entry-subtitle">Preview</h4>
24
+ </summary>
25
+ <p class="vs-entry-preview-link">
26
+ <%= link_to "View in new window", preview.to_path(params: {params: request_params}), target: "_blank" %>
27
+ </p>
28
+ <div class="vs-preview">
29
+ <%= preview %>
30
+ </div>
31
+ </details>
32
+ <% end %>
33
+ <% if details.include?(:source) %>
34
+ <details class="vs-entry-details vs-entry-details--source" open>
35
+ <summary>
36
+ <h4 class="vs-entry-subtitle">Source</h4>
37
+ </summary>
38
+ <code class="vs-code-block" data-lang="<%= preview.lang %>">
39
+ <pre><%= preview.source %></pre>
40
+ </code>
41
+ </details>
42
+ <% end %>
43
+ <% if details.include?(:html) %>
44
+ <details class="vs-entry-details vs-entry-details--html">
45
+ <summary>
46
+ <h4 class="vs-entry-subtitle">HTML output</h4>
47
+ </summary>
48
+ <code class="vs-code-block" data-lang="html">
49
+ <pre><%= preview.raw %></pre>
50
+ </code>
51
+ </details>
52
+ <% end %>
53
+ <% end %>
54
+ </div>
55
+ </section>
@@ -0,0 +1,18 @@
1
+ <%# locals: (spec:, params: {}, header: true, details: true) -%>
2
+
3
+ <article class="vs-spec" data-view-spec data-view-spec-subject="<%= spec.subject %>">
4
+ <% if header %>
5
+ <header class="vs-spec-header">
6
+ <h2 class="vs-spec-title"><%= spec.title %></h2>
7
+ </header>
8
+ <% end %>
9
+ <div class="vs-spec-content">
10
+ <div class="vs-spec-entries">
11
+ <% spec.entries.each do |entry| %>
12
+ <div class="vs-spec-entry">
13
+ <%= render entry, params:, details: %>
14
+ </div>
15
+ <% end %>
16
+ </div>
17
+ </div>
18
+ </article>
@@ -0,0 +1,14 @@
1
+ <%# locals: {group:, notes: nil} %>
2
+ <% scenarios_with_notes = group.scenarios.filter { _1.notes? } %>
3
+ <%= notes if notes.present? %>
4
+
5
+
6
+
7
+ <% scenarios_with_notes.each do |scenario| %>
8
+ <div style="all: revert; padding-left: 0.5rem">
9
+ <h4 style="all: revert; margin: 0 0 0.5rem -0.5rem;"><%= scenario.title %></h4>
10
+ <%= scenario.notes %>
11
+ </div>
12
+ <% end %>
13
+
14
+
@@ -0,0 +1,19 @@
1
+ <%# locals: {group:, params: {}} %>
2
+
3
+ <% group.scenarios.each do |scenario| %>
4
+ <% preview = scenario.preview(params:) %>
5
+ <div style="display: block;<%= " margin-bottom: 1.5rem;" unless scenario == group.scenarios.last %>">
6
+ <h6 style="all: revert; display: flex; align-items: center; font-family: ui-monospace, monospace; font-weight: normal; font-size: 0.9rem; margin-top: 0; margin-bottom: 0.8rem; color: currentColor">
7
+ <span style="all: revert; margin-right: 0.3rem; color: currentColor"><%= scenario.title %></span>
8
+ <a style="all: revert; display: flex; align-items: center; text-decoration: underline; cursor: pointer; opacity: 0.7;" href="<%= preview.to_path %>" target="_blank" title="Open in new window">
9
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="unset: all; display: inline-block;">
10
+ <path d="M21 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6"/><path d="m21 3-9 9"/>
11
+ <path d="M15 3h6v6"/>
12
+ </svg>
13
+ </a>
14
+ </h6>
15
+ <div>
16
+ <%= preview %>
17
+ </div>
18
+ </div>
19
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: {group:, params: {}} %>
2
+
3
+ <% group.scenarios.each do |scenario| %>
4
+ <!-- <%= scenario.title %> -->
5
+ <%= scenario.preview(params:) %>
6
+
7
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: {group:, params: {}} %>
2
+
3
+ <% group.scenarios.each do |scenario| %>
4
+ # <%= scenario.title %>
5
+ <%= scenario.preview(params:).source %>
6
+
7
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% if @view_specs.any? %>
2
+ <ul id="view-spec-previews">
3
+ <% @view_specs.each do |view_spec| %>
4
+ <li>
5
+ <h2><%= view_spec.title %></h2>
6
+ <% if view_spec.previews? %>
7
+ <ul>
8
+ <% view_spec.entries.each do |entry| %>
9
+ <% if entry.preview? %>
10
+ <li>
11
+ <%= link_to entry.title, entry.preview.to_path %>
12
+ </li>
13
+ <% end %>
14
+ <% end %>
15
+ </ul>
16
+ <% else %>
17
+ <p><em>No previews added.</em></p>
18
+ <% end %>
19
+ </li>
20
+ <% end%>
21
+ </ul>
22
+ <% end %>
@@ -0,0 +1,43 @@
1
+ module ViewSpec
2
+ module Collection
3
+ extend ActiveSupport::Concern
4
+ include Enumerable
5
+
6
+ included do
7
+ attr_reader :items
8
+ delegate_missing_to :items
9
+ end
10
+
11
+ def initialize(items = [])
12
+ @items = items.is_a?(Collection) ? items.to_a : items
13
+ end
14
+
15
+ def find_where(conditions)
16
+ where(conditions).first
17
+ end
18
+
19
+ def find_where!(conditions)
20
+ result = find_where(conditions)
21
+ result.nil? ? raise("No matching item found for conditions #{conditions}") : result
22
+ end
23
+
24
+ def where(conditions)
25
+ conditions.reduce(items.dup) do |collection, (key, value)|
26
+ collection.filter! { _1.respond_to?(key) && (_1.public_send(key) == value) }
27
+ end
28
+ end
29
+
30
+ def <<(item)
31
+ items.push(item)
32
+ self
33
+ end
34
+
35
+ def +(other)
36
+ self.class.new(items + other.items)
37
+ end
38
+
39
+ def to_a
40
+ items
41
+ end
42
+ end
43
+ end