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
@@ -8,14 +8,33 @@ module ViewSpec
8
8
  config.preview_controller.is_a?(String) ? config.preview_controller.constantize : config.preview_controller
9
9
  end
10
10
 
11
+ def preview_controller_name
12
+ preview_controller = config.preview_controller.is_a?(Class) ? config.preview_controller.name : config.preview_controller
13
+ preview_controller.underscore.sub(/_controller$/, "")
14
+ end
15
+
11
16
  class << self
12
17
  def defaults
13
18
  ActiveSupport::OrderedOptions.new.merge!({
14
- spec_paths: ["#{Rails.root}/view_specs"],
19
+ spec_paths: ["#{Rails.root}/test/view_specs"],
15
20
  spec_files: [],
16
21
  spec_file_suffix: "_vspec",
17
- preview_controller: "ApplicationController",
18
- preview_layout: "application"
22
+
23
+ preview_controller: "ViewSpecPreviewsController",
24
+ preview_layout: nil,
25
+ preview_route: "rails/view_spec/previews",
26
+ show_previews: Rails.env.development? || Rails.env.test?,
27
+
28
+ markdown_extensions: {
29
+ tables: true,
30
+ fenced_code_blocks: true,
31
+ disable_indented_code_blocks: true,
32
+ strikethrough: true,
33
+ highlight: true,
34
+ with_toc_data: true,
35
+ lax_spacing: true,
36
+ escape_html: false
37
+ }
19
38
  })
20
39
  end
21
40
  end
@@ -25,5 +44,9 @@ module ViewSpec
25
44
  def config
26
45
  @config ||= self.class.defaults
27
46
  end
47
+
48
+ def constantize(arg)
49
+ arg.is_a?(String) ? arg.constantize : arg
50
+ end
28
51
  end
29
52
  end
@@ -0,0 +1,40 @@
1
+ module ViewSpec
2
+ class Context
3
+ def initialize(entry, defaults = {})
4
+ @entry = entry
5
+
6
+ defaults.to_h.each do |key, value|
7
+ instance_variable_set(:"@#{key}", value) if respond_to?(key, true)
8
+ end
9
+ end
10
+
11
+ def entries
12
+ @entries ||= EntryCollection.new
13
+ end
14
+
15
+ private
16
+
17
+ def cascading_values
18
+ self.class.cascading_attrs.map do |key|
19
+ value = send(key)
20
+ value = (value.is_a?(Hash) || value.is_a?(Array)) ? value.dup : value
21
+ [key, value]
22
+ end.to_h
23
+ end
24
+
25
+ def caller_lang(location, fallback = nil)
26
+ lineno = location&.lineno if location.present?
27
+ lineno ? @entry.source_file.lang_from_line(lineno, fallback) : fallback
28
+ end
29
+
30
+ class << self
31
+ def cascading_attr(name)
32
+ cascading_attrs << name.to_sym
33
+ end
34
+
35
+ def cascading_attrs
36
+ @cascading_attrs ||= []
37
+ end
38
+ end
39
+ end
40
+ end
@@ -4,12 +4,17 @@ module ViewSpec
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- lookup_attr :controller
7
+ cascading_attr :controller
8
8
  end
9
9
 
10
10
  def controller(value = nil)
11
11
  @controller = value unless value.nil?
12
- controller = @controller || @outer_scope&.controller || ViewSpec.config.preview_controller
12
+ controller_class(@controller || ViewSpec.config.preview_controller)
13
+ end
14
+
15
+ private
16
+
17
+ def controller_class(controller)
13
18
  controller.is_a?(String) ? controller.camelize.constantize : controller
14
19
  end
15
20
  end
@@ -4,11 +4,11 @@ module ViewSpec
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  def groups
7
- entries.filter { _1.is_a?(Group) }
7
+ GroupCollection.new(entries.filter { _1.type == :group }.to_a)
8
8
  end
9
9
 
10
10
  def group(name, &block)
11
- entries << Group.new(name, lookup_attrs, &block)
11
+ entries << Group.new(name, @entry, cascading_values, &block)
12
12
  entries.last
13
13
  end
14
14
  end
@@ -4,13 +4,19 @@ module ViewSpec
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- lookup_attr :layout
7
+ cascading_attr :layout
8
8
  end
9
9
 
10
10
  def layout(value = nil)
11
11
  @layout = value unless value.nil?
12
12
  @layout ||= ViewSpec.config.preview_layout
13
13
  end
14
+
15
+ private
16
+
17
+ def determine_layout(value = false)
18
+ (value == true) ? layout : value
19
+ end
14
20
  end
15
21
  end
16
22
  end
@@ -0,0 +1,22 @@
1
+ module ViewSpec
2
+ module DSL
3
+ module Notes
4
+ extend ActiveSupport::Concern
5
+
6
+ def notes(str = nil)
7
+ if str.nil?
8
+ @notes
9
+ else
10
+ executable = ExecutableString.new(str, caller_lang(caller_locations(1..1)&.first, :md))
11
+ @notes = Note.new(@entry, executable)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def notes?
18
+ notes.present?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -3,34 +3,44 @@ module ViewSpec
3
3
  module Params
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ included do
7
+ cascading_attr :params_data
8
+ end
9
+
6
10
  def param(name, cast_type = nil, **options)
7
- param_definitions << {name:, cast_type:, options:}
11
+ params_data[name] = ParamData.new(cast_type, options)
8
12
  end
9
13
 
10
- def params
11
- raise ":params cannot be accessed outside of the preview context"
14
+ def params_data
15
+ @params_data ||= {}
12
16
  end
13
17
 
18
+ private
19
+
14
20
  def resolve_params(values_hash = {})
15
- values_hash = values_hash.to_h.with_indifferent_access
16
- param_set = ParamSet.new
21
+ values_hash = values_hash.to_h.deep_symbolize_keys
22
+ return values_hash unless params_data.is_a?(Hash)
17
23
 
18
- param_definitions.each do |props|
19
- value = values_hash[props[:name]]
20
- param_set.add(props[:name], props[:cast_type], value:, **props[:options])
24
+ param_set = ParamSet.new
25
+ params_data.each do |key, param|
26
+ value = values_hash[key]
27
+ param_set.add(key, param.cast_type, value:, **param.options)
21
28
  end
22
29
 
23
- other_params = values_hash.reject { param_set.include?(_1) }
24
- param_set.add(other_params)
25
-
26
30
  param_set
27
31
  end
28
32
 
29
- private
30
-
31
- def param_definitions
32
- @param_definitions ||= []
33
+ def validate_params(values_hash = {})
34
+ required = resolve_params(values_hash).reject { _2.default? }
35
+ missing_names = required.map(&:first).reject { values_hash.key?(_1) }
36
+ if missing_names.any?
37
+ raise "Missing #{"param".pluralize(missing_names.size)} value for #{missing_names.join(", ")}"
38
+ else
39
+ true
40
+ end
33
41
  end
42
+
43
+ ParamData = Struct.new(:cast_type, :options)
34
44
  end
35
45
  end
36
46
  end
@@ -0,0 +1,25 @@
1
+ module ViewSpec
2
+ module DSL
3
+ module Preview
4
+ extend ActiveSupport::Concern
5
+
6
+ def preview(tpl = nil, &block)
7
+ return @preview if tpl.nil? && !block_given?
8
+
9
+ @preview = if block
10
+ ExecutableProc.new(block)
11
+ elsif tpl.is_a?(String)
12
+ ExecutableString.new(tpl, caller_lang(caller_locations(1..1)&.first, :erb))
13
+ else
14
+ raise "Invalid preview argument (must be string or block)"
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def preview?
21
+ preview.present?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -2,19 +2,19 @@ module ViewSpec
2
2
  module DSL
3
3
  module Scenarios
4
4
  extend ActiveSupport::Concern
5
-
6
- def scenarios(include_grouped: false)
7
- entries.map do |entry|
5
+ def scenarios(flatten: false)
6
+ items = entries.map do |entry|
8
7
  if entry.type == :scenario
9
8
  entry
10
- elsif include_grouped && entry.type == :group
11
- entry.scenarios
9
+ elsif flatten && entry.type == :group
10
+ entry.scenarios.to_a
12
11
  end
13
12
  end.compact.flatten
13
+ ScenarioCollection.new(items.to_a)
14
14
  end
15
15
 
16
16
  def scenario(name, &block)
17
- entries << Scenario.new(name, lookup_attrs, &block)
17
+ entries << Scenario.new(name, @entry, cascading_values, &block)
18
18
  entries.last
19
19
  end
20
20
  end
@@ -0,0 +1,18 @@
1
+ module ViewSpec
2
+ module DSL
3
+ module Tests
4
+ extend ActiveSupport::Concern
5
+
6
+ def tests(&block)
7
+ @tests = block if block
8
+ @tests
9
+ end
10
+
11
+ private
12
+
13
+ def tests?
14
+ tests.present?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -5,7 +5,7 @@ module ViewSpec
5
5
 
6
6
  def title(value = nil)
7
7
  @title = value unless value.nil?
8
- @title ||= @subject&.to_title
8
+ @title
9
9
  end
10
10
  end
11
11
  end
@@ -7,8 +7,27 @@ module ViewSpec
7
7
  config.view_spec = ViewSpec.config
8
8
 
9
9
  config.after_initialize do |app|
10
+ opts = app.config.view_spec
11
+
12
+ if opts.show_previews
13
+ app.routes.prepend do
14
+ get(
15
+ opts.preview_route,
16
+ to: "#{opts.preview_controller_name}#index",
17
+ as: :view_spec_previews,
18
+ internal: true
19
+ )
20
+
21
+ get(
22
+ "#{opts.preview_route}/*spec@:entry",
23
+ to: "#{opts.preview_controller_name}#preview",
24
+ as: :view_spec_preview,
25
+ internal: true
26
+ )
27
+ end
28
+ end
29
+
10
30
  if !app.config.cache_classes
11
- opts = config.view_spec
12
31
  reloader = Reloader.new(opts.spec_files, opts.spec_paths) do
13
32
  ViewSpec.load!
14
33
  end
@@ -0,0 +1,62 @@
1
+ module ViewSpec
2
+ class Entry
3
+ delegate :type, to: :class
4
+
5
+ def initialize(subject, parent, context_defaults = {}, &block)
6
+ @context_defaults = context_defaults.to_h
7
+ @subject = Subject.new(subject)
8
+ @parent = parent
9
+
10
+ context.instance_exec(&block) if block
11
+ end
12
+
13
+ def title
14
+ if context.respond_to?(:title)
15
+ context.title || @subject.to_title
16
+ else
17
+ @subject.to_title
18
+ end
19
+ end
20
+
21
+ def short_identifier
22
+ @subject.to_short_identifier
23
+ end
24
+
25
+ alias_method :name, :short_identifier
26
+
27
+ def spec
28
+ @parent ? @parent.spec : self
29
+ end
30
+
31
+ def source_file
32
+ spec.source_file
33
+ end
34
+
35
+ def to_partial_path
36
+ "view_specs/#{type}"
37
+ end
38
+
39
+ def respond_to_missing?(name, include_private = false)
40
+ context.respond_to?(name, true) || super
41
+ end
42
+
43
+ def method_missing(name, ...)
44
+ context.respond_to?(name, true) ? context.send(name, ...) : super
45
+ end
46
+
47
+ private
48
+
49
+ def context
50
+ @context ||= begin
51
+ context_class = "#{self.class.name}Context".constantize
52
+ context_class.new(self, @context_defaults)
53
+ end
54
+ end
55
+
56
+ class << self
57
+ def type
58
+ @type ||= name.demodulize.underscore.to_sym
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ module ViewSpec
2
+ class EntryCollection
3
+ include Collection
4
+
5
+ def find(name = nil, &block)
6
+ return items.find(&block) if name.nil?
7
+
8
+ find_where({name: name.to_s}) || find_where({subject: name})
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,8 @@
1
1
  module ViewSpec
2
2
  class Error < StandardError
3
3
  end
4
+
5
+ class NoPreviewError < StandardError; end
6
+
7
+ class NoTestsError < StandardError; end
4
8
  end
@@ -0,0 +1,32 @@
1
+ module ViewSpec
2
+ class ExecutableProc
3
+ def initialize(proc)
4
+ @executable = proc
5
+ end
6
+
7
+ def source
8
+ CGI.unescapeHTML inner_source.strip_heredoc.strip
9
+ end
10
+
11
+ def lang
12
+ :ruby
13
+ end
14
+
15
+ def to_proc
16
+ @executable
17
+ end
18
+
19
+ alias_method :raw, :to_proc
20
+
21
+ private
22
+
23
+ def inner_source
24
+ source = @executable.source.strip_heredoc
25
+ if source.match?(/^(\w+)\s*\{/)
26
+ source.match(/^(\w+)\s*\{(.*)\}\s?$/m)[2]
27
+ elsif source.match?(/^(\w+)\s*do/)
28
+ source.match(/^(\w+)\s*do(.*)end\s?$/m)[2]
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module ViewSpec
2
+ class ExecutableString
3
+ attr_reader :lang
4
+
5
+ def initialize(str, lang = nil)
6
+ @str = str
7
+ @executable = lambda { render inline: str, type: lang || :erb }
8
+ @lang = lang
9
+ end
10
+
11
+ def source
12
+ CGI.unescapeHTML @str.strip_heredoc.strip
13
+ end
14
+
15
+ def to_proc
16
+ @executable
17
+ end
18
+
19
+ def raw
20
+ @str
21
+ end
22
+ end
23
+ end
@@ -1,24 +1,28 @@
1
1
  module ViewSpec
2
- class Group < DSL::Context
3
- include DSL::Title
4
- include DSL::Layout
5
- include DSL::Scenarios
6
- include DSL::Controller
2
+ class Group < Entry
3
+ def preview(**options)
4
+ GroupPreview.new(self, **options)
5
+ end
7
6
 
8
- def short_identifier
9
- @subject.to_short_identifier
7
+ def preview?
8
+ scenarios.any? { _1.preview? }
10
9
  end
11
10
 
12
- def spec
13
- @outer_scope
11
+ def notes
12
+ if context.notes.present? || scenarios.filter { _1.notes? }.any?
13
+ ViewSpec.config.preview_controller.render(
14
+ partial: "view_specs/group/notes",
15
+ locals: {group: self, notes: context.notes}
16
+ )
17
+ end
14
18
  end
15
19
 
16
- def to_param
17
- short_identifier
20
+ def notes?
21
+ notes.present?
18
22
  end
19
23
 
20
- def to_partial_path
21
- "view_spec/group"
24
+ def to_param
25
+ short_identifier
22
26
  end
23
27
  end
24
28
  end
@@ -0,0 +1,4 @@
1
+ module ViewSpec
2
+ class GroupCollection < EntryCollection
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module ViewSpec
2
+ class GroupContext < Context
3
+ include DSL::Title
4
+ include DSL::Notes
5
+ include DSL::Layout
6
+ include DSL::Params
7
+ include DSL::Scenarios
8
+ include DSL::Controller
9
+ end
10
+ end
@@ -0,0 +1,52 @@
1
+ module ViewSpec
2
+ class GroupPreview < Preview
3
+ PARTIAL_PATHS = {
4
+ raw: "view_specs/group/raw",
5
+ source: "view_specs/group/source",
6
+ preview: "view_specs/group/preview"
7
+ }
8
+
9
+ def initialize(entry, partial: nil, **options)
10
+ @partial = partial
11
+ super(entry, **options)
12
+ end
13
+
14
+ def call(view_context = nil)
15
+ render_in_layout(:preview, view_context).html_safe
16
+ end
17
+
18
+ def raw(view_context = nil)
19
+ html = render_in_layout(:raw, view_context)
20
+ CGI.unescapeHTML(html)
21
+ end
22
+
23
+ def source(view_context = nil)
24
+ render_in_layout(:source, view_context)
25
+ end
26
+
27
+ def lang
28
+ @entry.scenarios&.first&.preview&.lang || :ruby
29
+ end
30
+
31
+ alias_method :to_s, :call
32
+ alias_method :to_str, :call
33
+
34
+ private
35
+
36
+ attr_reader :assigns
37
+
38
+ def partial_for_format(format)
39
+ @partial.nil? ? PARTIAL_PATHS[format.to_sym] : @partial
40
+ end
41
+
42
+ def render_in_layout(format, view_context = nil)
43
+ @_output ||= {}
44
+ @_output[format] ||= begin
45
+ partial = partial_for_format(format)
46
+ locals = {group: @entry, params:}
47
+
48
+ renderer.render(partial:, locals:, assigns:, layout:).strip
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,20 @@
1
+ module ViewSpec
2
+ class Markdown
3
+ def initialize
4
+ renderer = ViewSpec::MarkdownRenderer.new
5
+ @markdown = Redcarpet::Markdown.new(renderer, extensions)
6
+ end
7
+
8
+ def render(text = nil, &block)
9
+ text ||= block&.call
10
+
11
+ @markdown.render(text).html_safe unless text.nil?
12
+ end
13
+
14
+ private
15
+
16
+ def extensions
17
+ ViewSpec.config.markdown_extensions || {}
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module ViewSpec
2
+ class MarkdownRenderer < Redcarpet::Render::HTML
3
+ HTML_ELEMENT_MATCHER = /^(<([a-z\-]+)(?:\s[^>]*)?>((?:(?!<\/([a-z\-]+)>).)*)<\/([a-z\-]+)>)$/m
4
+
5
+ def paragraph(content)
6
+ if HTML_ELEMENT_MATCHER.match?(content.strip)
7
+ content.html_safe
8
+ else
9
+ "<p>#{content}</p>".html_safe
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module ViewSpec
2
+ module Minitest
3
+ module Tests
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Execute tests in a Minitest test context.
8
+ # Tests are grouped by spec and scenario using nested classes.
9
+ ViewSpec.specs.each do |spec|
10
+ scenarios = spec.scenarios(flatten: true).filter(&:tests?)
11
+ if scenarios.any?
12
+ class_options = {base_class: ViewSpec::TestCase}
13
+ Utils.create_class(spec.lookup_path, self, **class_options) do
14
+ scenarios.each do |scenario|
15
+ test_case_name = "#{scenario.short_identifier.camelize}Test"
16
+ test_case_class = Utils.create_class(test_case_name, self, **class_options) do
17
+ define_method(:scenario) { scenario }
18
+ end
19
+ test_case_class.instance_exec(&scenario.tests)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end