view_spec 0.0.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/app/views/view_spec/_group.html.erb +20 -0
- data/app/views/view_spec/_scenario.html.erb +17 -0
- data/app/views/view_spec/_spec.html.erb +10 -0
- data/lib/view_spec/config.rb +29 -0
- data/lib/view_spec/dsl/context.rb +54 -0
- data/lib/view_spec/dsl/controller.rb +17 -0
- data/lib/view_spec/dsl/groups.rb +16 -0
- data/lib/view_spec/dsl/layout.rb +16 -0
- data/lib/view_spec/dsl/params.rb +36 -0
- data/lib/view_spec/dsl/scenarios.rb +22 -0
- data/lib/view_spec/dsl/title.rb +12 -0
- data/lib/view_spec/dsl.rb +18 -0
- data/lib/view_spec/engine.rb +23 -0
- data/lib/view_spec/error.rb +4 -0
- data/lib/view_spec/group.rb +24 -0
- data/lib/view_spec/helpers/spec_helper.rb +9 -0
- data/lib/view_spec/param.rb +34 -0
- data/lib/view_spec/param_set.rb +32 -0
- data/lib/view_spec/registry.rb +54 -0
- data/lib/view_spec/reloader.rb +15 -0
- data/lib/view_spec/renderable.rb +36 -0
- data/lib/view_spec/scenario.rb +58 -0
- data/lib/view_spec/source_file.rb +55 -0
- data/lib/view_spec/spec.rb +37 -0
- data/lib/view_spec/subject.rb +42 -0
- data/lib/view_spec/type.rb +39 -0
- data/lib/view_spec/types/boolean.rb +6 -0
- data/lib/view_spec/types/date.rb +6 -0
- data/lib/view_spec/types/date_time.rb +6 -0
- data/lib/view_spec/types/decimal.rb +6 -0
- data/lib/view_spec/types/float.rb +6 -0
- data/lib/view_spec/types/integer.rb +6 -0
- data/lib/view_spec/types/string.rb +6 -0
- data/lib/view_spec/types/symbol.rb +9 -0
- data/lib/view_spec/types/time.rb +6 -0
- data/lib/view_spec/utils.rb +9 -0
- data/lib/view_spec/version.rb +3 -0
- data/lib/view_spec.rb +38 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: db2b2ddc540389235a0c75db6bf93c85e65b854574623ea09b782e24d330d6ad
|
4
|
+
data.tar.gz: f7c9400e0487de9c32c2cd0fa345c4c29e6bdceae3143bdb310c0bd2568c0390
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e8fe0a339f4f6e8b22c212f5f21d70c66bdedf8e1ffb1c56b03ce236025185a2b28a0cce1510fec59ceeab038550689016d998dce823af51d30b61d83c68f3f1
|
7
|
+
data.tar.gz: 6d720f348d61915a323f0217d30af8fa09cb633918a13d4480e65db52c1433dc37b552b5e3abc407eff994e464f5212e90a8d990389d34fb185fe0acea3ab315
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021-present Mark Perkins
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# ViewSpec
|
2
|
+
|
3
|
+
ViewSpec lets you create example-led specifications for components, partials and views using an expressive Ruby DSL.
|
4
|
+
|
5
|
+
Specs can then be used in your tests, to render previews for development and visual testing or to generate documentation.
|
6
|
+
|
7
|
+
```rb
|
8
|
+
# button_vspec.rb
|
9
|
+
|
10
|
+
ViewSpec.spec ButtonComponent do
|
11
|
+
title "Button (ViewComponent)"
|
12
|
+
|
13
|
+
scenario "default" do
|
14
|
+
preview do
|
15
|
+
render ButtonComponent.new(text: "Button text")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
scenario "editable params" do
|
20
|
+
param :text, :string, default: "Button text"
|
21
|
+
param :type, :symbol, default: :button
|
22
|
+
|
23
|
+
preview do
|
24
|
+
render ButtonComponent.new(**params)
|
25
|
+
end
|
26
|
+
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
|
36
|
+
|
37
|
+
group "themes" do
|
38
|
+
scenario "primary" do
|
39
|
+
preview { render ButtonComponent.new(text: "Primary", theme: :primary) }
|
40
|
+
end
|
41
|
+
|
42
|
+
scenario "secondary" do
|
43
|
+
preview { render ButtonComponent.new(text: "Secondary", theme: :secondary) }
|
44
|
+
end
|
45
|
+
|
46
|
+
scenario "danger" do
|
47
|
+
preview { render ButtonComponent.new(text: "Danger", theme: :danger) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%# locals: (group:) %>
|
2
|
+
|
3
|
+
<section style="padding: 20px; border: 1px solid #ccc; margin-bottom: 20px;">
|
4
|
+
<header>
|
5
|
+
<h2><%= group.title %></h2>
|
6
|
+
<p>Short identifier: <%= group.short_identifier %></p>
|
7
|
+
</header>
|
8
|
+
<div style="padding-left: 0px;">
|
9
|
+
<h4>Preview:</h4>
|
10
|
+
<div style="padding: 20px; border: 2px dotted #bbb;">
|
11
|
+
<% group.scenarios.each do |scenario| %>
|
12
|
+
<%= scenario.render_preview(params: request.query_parameters) %>
|
13
|
+
<% end %>
|
14
|
+
</div>
|
15
|
+
<h4>Output:</h4>
|
16
|
+
<code>
|
17
|
+
<pre style="padding: 10px; background-color: #eee; display: block;"><% group.scenarios.each do |scenario| -%><%= scenario.render_output(params: request.query_parameters) + "\n" %><% end %></pre>
|
18
|
+
</code>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<%# locals: (scenario:) %>
|
2
|
+
|
3
|
+
<section style="padding: 20px; border: 1px solid #ccc; margin-bottom: 20px;">
|
4
|
+
<header>
|
5
|
+
<h2><%= scenario.title %></h2>
|
6
|
+
<p>Short identifier: <%= scenario.short_identifier %></p>
|
7
|
+
</header>
|
8
|
+
|
9
|
+
<h4>Preview:</h4>
|
10
|
+
<div style="padding: 20px; border: 2px dotted #bbb;">
|
11
|
+
<%= scenario.render_preview(params: request.query_parameters) %>
|
12
|
+
</div>
|
13
|
+
<h4>Output:</h4>
|
14
|
+
<code>
|
15
|
+
<pre style="padding: 10px; background-color: #eee; display: block;"><%= scenario.render_output(params: request.query_parameters) %></pre>
|
16
|
+
</code>
|
17
|
+
</section>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Config
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
delegate_missing_to :config
|
6
|
+
|
7
|
+
def preview_controller
|
8
|
+
config.preview_controller.is_a?(String) ? config.preview_controller.constantize : config.preview_controller
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def defaults
|
13
|
+
ActiveSupport::OrderedOptions.new.merge!({
|
14
|
+
spec_paths: ["#{Rails.root}/view_specs"],
|
15
|
+
spec_files: [],
|
16
|
+
spec_file_suffix: "_vspec",
|
17
|
+
preview_controller: "ApplicationController",
|
18
|
+
preview_layout: "application"
|
19
|
+
})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def config
|
26
|
+
@config ||= self.class.defaults
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
class Context
|
4
|
+
delegate :type, to: :class
|
5
|
+
|
6
|
+
def initialize(subject, lookup_attrs = {}, &block)
|
7
|
+
@subject = Subject.new(subject)
|
8
|
+
set_lookup_attrs(lookup_attrs.to_h)
|
9
|
+
@block = block
|
10
|
+
|
11
|
+
evaluate!
|
12
|
+
end
|
13
|
+
|
14
|
+
def entries
|
15
|
+
@entries ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def type
|
20
|
+
@type ||= name.demodulize.underscore.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def lookup_attr(name)
|
24
|
+
lookup_attrs << name.to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
def lookup_attrs
|
28
|
+
@lookup_attrs ||= []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def lookup_attrs
|
35
|
+
self.class.lookup_attrs.map do |key|
|
36
|
+
[key, public_send(key)]
|
37
|
+
end.to_h
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_lookup_attrs(values_hash)
|
41
|
+
values_hash.each do |key, value|
|
42
|
+
public_send(key, value) if respond_to?(key)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def evaluate!
|
47
|
+
return if @evaluated
|
48
|
+
|
49
|
+
instance_exec(&@block) if @block
|
50
|
+
@evaluated = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
module Controller
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
lookup_attr :controller
|
8
|
+
end
|
9
|
+
|
10
|
+
def controller(value = nil)
|
11
|
+
@controller = value unless value.nil?
|
12
|
+
controller = @controller || @outer_scope&.controller || ViewSpec.config.preview_controller
|
13
|
+
controller.is_a?(String) ? controller.camelize.constantize : controller
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
module Groups
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def groups
|
7
|
+
entries.filter { _1.is_a?(Group) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def group(name, &block)
|
11
|
+
entries << Group.new(name, lookup_attrs, &block)
|
12
|
+
entries.last
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
module Layout
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
lookup_attr :layout
|
8
|
+
end
|
9
|
+
|
10
|
+
def layout(value = nil)
|
11
|
+
@layout = value unless value.nil?
|
12
|
+
@layout ||= ViewSpec.config.preview_layout
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
module Params
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def param(name, cast_type = nil, **options)
|
7
|
+
param_definitions << {name:, cast_type:, options:}
|
8
|
+
end
|
9
|
+
|
10
|
+
def params
|
11
|
+
raise ":params cannot be accessed outside of the preview context"
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve_params(values_hash = {})
|
15
|
+
values_hash = values_hash.to_h.with_indifferent_access
|
16
|
+
param_set = ParamSet.new
|
17
|
+
|
18
|
+
param_definitions.each do |props|
|
19
|
+
value = values_hash[props[:name]]
|
20
|
+
param_set.add(props[:name], props[:cast_type], value:, **props[:options])
|
21
|
+
end
|
22
|
+
|
23
|
+
other_params = values_hash.reject { param_set.include?(_1) }
|
24
|
+
param_set.add(other_params)
|
25
|
+
|
26
|
+
param_set
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def param_definitions
|
32
|
+
@param_definitions ||= []
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
module Scenarios
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def scenarios(include_grouped: false)
|
7
|
+
entries.map do |entry|
|
8
|
+
if entry.type == :scenario
|
9
|
+
entry
|
10
|
+
elsif include_grouped && entry.type == :group
|
11
|
+
entry.scenarios
|
12
|
+
end
|
13
|
+
end.compact.flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
def scenario(name, &block)
|
17
|
+
entries << Scenario.new(name, lookup_attrs, &block)
|
18
|
+
entries.last
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
module DSL
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def spec(subject, &block)
|
7
|
+
source_path = caller_locations(1..1).first.absolute_path
|
8
|
+
registry.add(source_path, subject, &block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def registry
|
12
|
+
@registry ||= Registry.new
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :specs, :registry
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "rails"
|
2
|
+
|
3
|
+
module ViewSpec
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
isolate_namespace ViewSpec
|
6
|
+
|
7
|
+
config.view_spec = ViewSpec.config
|
8
|
+
|
9
|
+
config.after_initialize do |app|
|
10
|
+
if !app.config.cache_classes
|
11
|
+
opts = config.view_spec
|
12
|
+
reloader = Reloader.new(opts.spec_files, opts.spec_paths) do
|
13
|
+
ViewSpec.load!
|
14
|
+
end
|
15
|
+
|
16
|
+
Rails.application.reloaders << reloader
|
17
|
+
reloader.execute
|
18
|
+
else
|
19
|
+
ViewSpec.load!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Group < DSL::Context
|
3
|
+
include DSL::Title
|
4
|
+
include DSL::Layout
|
5
|
+
include DSL::Scenarios
|
6
|
+
include DSL::Controller
|
7
|
+
|
8
|
+
def short_identifier
|
9
|
+
@subject.to_short_identifier
|
10
|
+
end
|
11
|
+
|
12
|
+
def spec
|
13
|
+
@outer_scope
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_param
|
17
|
+
short_identifier
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_partial_path
|
21
|
+
"view_spec/group"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "active_model"
|
2
|
+
module ViewSpec
|
3
|
+
class Param
|
4
|
+
include ::ActionView::Helpers::SanitizeHelper
|
5
|
+
|
6
|
+
attr_reader :name, :options
|
7
|
+
|
8
|
+
def initialize(name, cast_type = nil, value: nil, default: nil, **type_options)
|
9
|
+
@name = name.to_sym
|
10
|
+
@cast_type = cast_type
|
11
|
+
@value = value
|
12
|
+
@default = default
|
13
|
+
@type_options = type_options
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
type.cast(raw_value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
@type ||= Type.for(@cast_type, **@type_options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_pair
|
25
|
+
[name, value]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def raw_value
|
31
|
+
sanitize(@value) || @default
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class ParamSet
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
delegate :each, to: :@params
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@params = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(name, cast_type = nil, value: nil, **options)
|
12
|
+
if name.is_a?(Hash)
|
13
|
+
@params += name.map { Param.new(_1, value: _2) }
|
14
|
+
else
|
15
|
+
@params << Param.new(name, cast_type, value:, **options)
|
16
|
+
end
|
17
|
+
@params.uniq!(&:name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def names
|
21
|
+
@params.map(&:name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def include?(name)
|
25
|
+
names.include?(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_h
|
29
|
+
@params.map { _1.to_pair }.to_h
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Registry
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
delegate :each, to: :@specs
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@specs = []
|
9
|
+
@source_files = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(source_path, subject, &block)
|
13
|
+
delete(subject)
|
14
|
+
|
15
|
+
source_file = source_file(source_path)
|
16
|
+
@specs << Spec.new(source_file, subject, &block)
|
17
|
+
@specs.last
|
18
|
+
end
|
19
|
+
|
20
|
+
def source_file(path)
|
21
|
+
@source_files[path.to_s] ||= SourceFile.new(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(subject)
|
25
|
+
find_where({subject:})
|
26
|
+
end
|
27
|
+
|
28
|
+
def has?(...)
|
29
|
+
!!find(...)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(subject)
|
33
|
+
index = @specs.index { _1.subject == subject }
|
34
|
+
@specs.delete_at(index) unless index.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear!
|
38
|
+
@specs.clear
|
39
|
+
@source_files.clear
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_where(conditions)
|
43
|
+
where(conditions).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def where(conditions)
|
47
|
+
@specs.filter do |spec|
|
48
|
+
!!conditions.each do |key, value|
|
49
|
+
break false unless spec.respond_to?(key) && (spec.public_send(key) == value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Reloader
|
3
|
+
delegate :execute, to: :@file_watcher
|
4
|
+
|
5
|
+
def initialize(files = [], paths = [], &callback)
|
6
|
+
paths_hash = paths.map { [_1, ["rb"]] }.to_h
|
7
|
+
|
8
|
+
@file_watcher = ActiveSupport::FileUpdateChecker.new(files, paths_hash, &callback)
|
9
|
+
end
|
10
|
+
|
11
|
+
def updated?
|
12
|
+
@file_watcher.execute_if_updated
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Renderable
|
3
|
+
class CurrentTemplate
|
4
|
+
attr_accessor :short_identifier
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(identifier, tpl = nil, &block)
|
8
|
+
@identifier = identifier
|
9
|
+
@block = tpl ? -> { render inline: tpl } : block
|
10
|
+
end
|
11
|
+
|
12
|
+
def render_in(view_context)
|
13
|
+
render_context(view_context).instance_exec(&@block)&.strip&.html_safe
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def render_context(view_context)
|
19
|
+
view_context = view_context.clone
|
20
|
+
|
21
|
+
unless view_context.instance_variable_get(:@current_template)
|
22
|
+
current_template = CurrentTemplate.new
|
23
|
+
current_template.short_identifier = @identifier
|
24
|
+
view_context.instance_variable_set(:@current_template, current_template)
|
25
|
+
end
|
26
|
+
|
27
|
+
view_context.assigns.each do |key, value|
|
28
|
+
view_context.define_singleton_method(key) do
|
29
|
+
instance_variable_get(:"@#{key}") || {}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
view_context
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Scenario < DSL::Context
|
3
|
+
include DSL::Title
|
4
|
+
include DSL::Params
|
5
|
+
include DSL::Layout
|
6
|
+
include DSL::Controller
|
7
|
+
|
8
|
+
class NoPreviewError < StandardError; end
|
9
|
+
|
10
|
+
def preview(tpl = nil, &block)
|
11
|
+
@preview = tpl || block
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_preview(params: {}, assigns: {}, layout: false, raw: false)
|
15
|
+
assigns[:params] = resolve_params(params).to_h
|
16
|
+
html = render(renderable_preview, layout:, assigns:)
|
17
|
+
|
18
|
+
raw ? CGI.unescapeHTML(html) : html
|
19
|
+
end
|
20
|
+
|
21
|
+
def render_output(*args, **kwargs)
|
22
|
+
render_preview(*args, **kwargs, raw: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
@subject.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def short_identifier
|
30
|
+
@subject.to_short_identifier
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_param
|
34
|
+
short_identifier
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_partial_path
|
38
|
+
"view_spec/scenario"
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def renderable_preview
|
44
|
+
if @preview.is_a?(String)
|
45
|
+
Renderable.new(short_identifier, @preview)
|
46
|
+
elsif @preview.is_a?(Proc)
|
47
|
+
Renderable.new(short_identifier, &@preview)
|
48
|
+
else
|
49
|
+
raise NoPreviewError, "no preview defined for scenario `#{short_identifier}`"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def render(*args, layout: false, assigns: {}, **kwargs, &block)
|
54
|
+
layout = self.layout if layout == true
|
55
|
+
controller.render(*args, layout:, assigns:, **kwargs, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class SourceFile
|
3
|
+
attr_reader :absolute_path
|
4
|
+
|
5
|
+
delegate_missing_to :absolute_path
|
6
|
+
delegate :to_s, to: :absolute_path
|
7
|
+
|
8
|
+
def initialize(absolute_path)
|
9
|
+
@absolute_path = Pathname(absolute_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def normalized_name
|
13
|
+
basename(".rb").to_s.delete_suffix(ViewSpec.config.spec_file_suffix)
|
14
|
+
end
|
15
|
+
|
16
|
+
def relative_path
|
17
|
+
spec_file? ? absolute_path.relative_path_from(base_path) : app_path
|
18
|
+
end
|
19
|
+
|
20
|
+
def lookup_path
|
21
|
+
relative_dirname = relative_path.dirname.to_s.delete_prefix(".")
|
22
|
+
[relative_dirname.presence, normalized_name].compact.join("/")
|
23
|
+
end
|
24
|
+
|
25
|
+
def app_path
|
26
|
+
absolute_path.relative_path_from(Rails.root)
|
27
|
+
end
|
28
|
+
|
29
|
+
def spec_file?
|
30
|
+
absolute_path.to_s.end_with?("#{ViewSpec.config.spec_file_suffix}.rb")
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :to_path, :to_s
|
34
|
+
alias_method :to_pathname, :absolute_path
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def base_path
|
39
|
+
base_paths.find { descendant_of?(_1) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def base_paths
|
43
|
+
Utils.normalize_paths(ViewSpec.config.spec_paths)
|
44
|
+
end
|
45
|
+
|
46
|
+
def descendant_of?(directory_path)
|
47
|
+
directory_segments = File.expand_path(directory_path).split("/")
|
48
|
+
path_segments.slice(0, directory_segments.size) == directory_segments
|
49
|
+
end
|
50
|
+
|
51
|
+
def path_segments
|
52
|
+
absolute_path.to_s.split("/")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Spec < DSL::Context
|
3
|
+
include DSL::Title
|
4
|
+
include DSL::Layout
|
5
|
+
include DSL::Groups
|
6
|
+
include DSL::Scenarios
|
7
|
+
include DSL::Controller
|
8
|
+
|
9
|
+
attr_reader :subject, :source_file
|
10
|
+
|
11
|
+
def initialize(source_file, subject, &block)
|
12
|
+
@source_file = source_file
|
13
|
+
|
14
|
+
super(subject, nil, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def short_identifier
|
18
|
+
@subject.to_short_identifier
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup_path
|
22
|
+
if @source_file.spec_file?
|
23
|
+
@source_file.lookup_path
|
24
|
+
else
|
25
|
+
short_identifier
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_param
|
30
|
+
lookup_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_partial_path
|
34
|
+
"view_spec/spec"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Subject
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
delegate :to_s, to: :value
|
6
|
+
|
7
|
+
attr_reader :value
|
8
|
+
|
9
|
+
def initialize(value)
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_title
|
14
|
+
@title ||= to_short_identifier.titleize
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_identifier
|
18
|
+
@identifier ||= to_parameterized_path
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_short_identifier
|
22
|
+
@short_identifier ||= to_identifier.split("/").last
|
23
|
+
end
|
24
|
+
|
25
|
+
def <=>(other)
|
26
|
+
if other.is_a?(self.class)
|
27
|
+
to_identifier.casecmp other.to_identifier
|
28
|
+
else
|
29
|
+
value <=> other
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_parameterized_path
|
34
|
+
to_path.parameterize
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_path
|
38
|
+
path = @value.to_s.underscore.gsub(/\s+/, "_").delete_suffix(".rb")
|
39
|
+
path.gsub(/(_component|\/component)$/, "")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ViewSpec
|
2
|
+
class Type
|
3
|
+
TYPES = %i[boolean date date_time decimal float integer string symbol time]
|
4
|
+
|
5
|
+
delegate :cast, to: :value_caster
|
6
|
+
|
7
|
+
def initialize(**options)
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def type
|
12
|
+
self.class.name.demodulize.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def for(type, ...)
|
17
|
+
return new(...) unless type.in?(TYPES)
|
18
|
+
|
19
|
+
"ViewSpec::Types::#{type.to_s.camelize}".constantize.new(...)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def value_caster
|
26
|
+
@value_caster ||= begin
|
27
|
+
"::ActiveModel::Type::#{self.class.name.demodulize}".constantize.new
|
28
|
+
rescue NameError
|
29
|
+
NoopValueCaster.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class NoopValueCaster
|
35
|
+
def cast(value)
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/view_spec.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require "zeitwerk"
|
2
|
+
require "active_support"
|
3
|
+
require "view_spec/version"
|
4
|
+
|
5
|
+
loader = Zeitwerk::Loader.for_gem
|
6
|
+
loader.tag = "view_spec"
|
7
|
+
loader.inflector.inflect "dsl" => "DSL"
|
8
|
+
loader.push_dir("#{__dir__}/view_spec", namespace: ViewSpec)
|
9
|
+
loader.collapse("#{__dir__}/view_spec/helpers")
|
10
|
+
loader.enable_reloading if ENV["RAILS_ENV"] == "development"
|
11
|
+
loader.setup
|
12
|
+
|
13
|
+
module ViewSpec
|
14
|
+
include DSL
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def config
|
18
|
+
@config ||= Config.instance
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure(&block)
|
22
|
+
yield config
|
23
|
+
end
|
24
|
+
|
25
|
+
def load!
|
26
|
+
registry.clear!
|
27
|
+
|
28
|
+
Utils.normalize_paths(config.spec_paths).each do |specs_path|
|
29
|
+
spec_files = Dir.glob("#{specs_path}/**/*#{config.spec_file_suffix}.rb")
|
30
|
+
spec_files.each { load _1 }
|
31
|
+
end
|
32
|
+
|
33
|
+
Utils.normalize_paths(config.spec_files).each { load _1 }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require "view_spec/engine"
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: view_spec
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Perkins
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: zeitwerk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activemodel
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '7.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '7.1'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- LICENSE.txt
|
62
|
+
- README.md
|
63
|
+
- app/views/view_spec/_group.html.erb
|
64
|
+
- app/views/view_spec/_scenario.html.erb
|
65
|
+
- app/views/view_spec/_spec.html.erb
|
66
|
+
- lib/view_spec.rb
|
67
|
+
- lib/view_spec/config.rb
|
68
|
+
- lib/view_spec/dsl.rb
|
69
|
+
- lib/view_spec/dsl/context.rb
|
70
|
+
- lib/view_spec/dsl/controller.rb
|
71
|
+
- lib/view_spec/dsl/groups.rb
|
72
|
+
- lib/view_spec/dsl/layout.rb
|
73
|
+
- lib/view_spec/dsl/params.rb
|
74
|
+
- lib/view_spec/dsl/scenarios.rb
|
75
|
+
- lib/view_spec/dsl/title.rb
|
76
|
+
- lib/view_spec/engine.rb
|
77
|
+
- lib/view_spec/error.rb
|
78
|
+
- lib/view_spec/group.rb
|
79
|
+
- lib/view_spec/helpers/spec_helper.rb
|
80
|
+
- lib/view_spec/param.rb
|
81
|
+
- lib/view_spec/param_set.rb
|
82
|
+
- lib/view_spec/registry.rb
|
83
|
+
- lib/view_spec/reloader.rb
|
84
|
+
- lib/view_spec/renderable.rb
|
85
|
+
- lib/view_spec/scenario.rb
|
86
|
+
- lib/view_spec/source_file.rb
|
87
|
+
- lib/view_spec/spec.rb
|
88
|
+
- lib/view_spec/subject.rb
|
89
|
+
- lib/view_spec/type.rb
|
90
|
+
- lib/view_spec/types/boolean.rb
|
91
|
+
- lib/view_spec/types/date.rb
|
92
|
+
- lib/view_spec/types/date_time.rb
|
93
|
+
- lib/view_spec/types/decimal.rb
|
94
|
+
- lib/view_spec/types/float.rb
|
95
|
+
- lib/view_spec/types/integer.rb
|
96
|
+
- lib/view_spec/types/string.rb
|
97
|
+
- lib/view_spec/types/symbol.rb
|
98
|
+
- lib/view_spec/types/time.rb
|
99
|
+
- lib/view_spec/utils.rb
|
100
|
+
- lib/view_spec/version.rb
|
101
|
+
homepage: https://github.com/lookbook-hq/view_spec
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
metadata: {}
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 3.1.0
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubygems_version: 3.3.3
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: ViewSpec lets you create example-led specifications for components, partials
|
124
|
+
and views using an expressive Ruby DSL
|
125
|
+
test_files: []
|