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
@@ -0,0 +1,44 @@
1
+ module ViewSpec
2
+ class Note
3
+ delegate :lang, to: :@executable
4
+
5
+ def initialize(entry, executable)
6
+ @entry = entry
7
+ @executable = executable
8
+ end
9
+
10
+ def call(view_context = nil)
11
+ @rendered ||= begin
12
+ text = renderer(view_context).render(renderable, assigns:, layout: false)
13
+ output = (lang == :md) ? markdown.render(text) : text
14
+ output.html_safe
15
+ end
16
+ end
17
+
18
+ def render_in(view_context)
19
+ call(view_context)
20
+ end
21
+
22
+ alias_method :to_s, :call
23
+ alias_method :to_str, :call
24
+
25
+ private
26
+
27
+ def renderer(view_context = nil)
28
+ view_context || @entry.controller
29
+ end
30
+
31
+ def renderable
32
+ executable = @executable.is_a?(ExecutableString) ? ExecutableString.new(@executable.raw, :erb) : @executable
33
+ @renderable ||= Renderable.new(@entry.short_identifier, executable)
34
+ end
35
+
36
+ def markdown
37
+ @markdown || Markdown.new
38
+ end
39
+
40
+ def assigns
41
+ {params: @entry.resolve_params}
42
+ end
43
+ end
44
+ end
@@ -3,9 +3,11 @@ module ViewSpec
3
3
  class Param
4
4
  include ::ActionView::Helpers::SanitizeHelper
5
5
 
6
+ NO_DEFAULT = Object.new
7
+
6
8
  attr_reader :name, :options
7
9
 
8
- def initialize(name, cast_type = nil, value: nil, default: nil, **type_options)
10
+ def initialize(name, cast_type = nil, value: nil, default: NO_DEFAULT, **type_options)
9
11
  @name = name.to_sym
10
12
  @cast_type = cast_type
11
13
  @value = value
@@ -21,6 +23,14 @@ module ViewSpec
21
23
  @type ||= Type.for(@cast_type, **@type_options)
22
24
  end
23
25
 
26
+ def default
27
+ (@default == NO_DEFAULT) ? nil : @default
28
+ end
29
+
30
+ def default?
31
+ @default != NO_DEFAULT
32
+ end
33
+
24
34
  def to_pair
25
35
  [name, value]
26
36
  end
@@ -28,7 +38,7 @@ module ViewSpec
28
38
  private
29
39
 
30
40
  def raw_value
31
- sanitize(@value) || @default
41
+ sanitize(@value) || default
32
42
  end
33
43
  end
34
44
  end
@@ -2,31 +2,37 @@ module ViewSpec
2
2
  class ParamSet
3
3
  include Enumerable
4
4
 
5
- delegate :each, to: :@params
5
+ delegate :key?, :keys, :each, to: :@params
6
6
 
7
7
  def initialize
8
- @params = []
8
+ @params = ActiveSupport::OrderedOptions.new.merge!({})
9
9
  end
10
10
 
11
11
  def add(name, cast_type = nil, value: nil, **options)
12
12
  if name.is_a?(Hash)
13
- @params += name.map { Param.new(_1, value: _2) }
13
+ name.each { add(_1, value: _2) }
14
14
  else
15
- @params << Param.new(name, cast_type, value:, **options)
15
+ @params[name.to_sym] = Param.new(name, cast_type, value:, **options)
16
16
  end
17
- @params.uniq!(&:name)
17
+ @params
18
18
  end
19
19
 
20
- def names
21
- @params.map(&:name)
20
+ def to_h
21
+ @params.transform_values { _1.value }
22
22
  end
23
23
 
24
- def include?(name)
25
- names.include?(name)
24
+ alias_method :to_hash, :to_h
25
+
26
+ def respond_to_missing?(name, include_private = false)
27
+ @params.key?(name)
26
28
  end
27
29
 
28
- def to_h
29
- @params.map { _1.to_pair }.to_h
30
+ def method_missing(name, ...)
31
+ if @params.key?(name)
32
+ @params.fetch(name).value
33
+ else
34
+ super
35
+ end
30
36
  end
31
37
  end
32
38
  end
@@ -0,0 +1,44 @@
1
+ module ViewSpec
2
+ class Preview
3
+ def initialize(entry, params: {}, assigns: {}, layout: false, identifier: nil)
4
+ @entry = entry
5
+ @params = params
6
+ @assigns = assigns
7
+ @layout = layout
8
+ end
9
+
10
+ def to_path(params: @params.to_h)
11
+ url_helpers.view_spec_preview_path(@entry.spec, @entry, {params:})
12
+ end
13
+
14
+ def to_url(params: @params.to_h)
15
+ url_helpers.view_spec_preview_url(@entry.spec, @entry, {params:})
16
+ end
17
+
18
+ def render_in(view_context)
19
+ call(view_context)
20
+ end
21
+
22
+ def call(view_context = nil)
23
+ raise "ViewSpec::Preview subclasses must implement the `call` method"
24
+ end
25
+
26
+ def params(validate: false)
27
+ @entry.resolve_params(@params)
28
+ end
29
+
30
+ def layout
31
+ @entry.determine_layout(@layout)
32
+ end
33
+
34
+ private
35
+
36
+ def renderer(view_context = nil)
37
+ view_context || @entry.controller
38
+ end
39
+
40
+ def url_helpers
41
+ Rails.application.routes.url_helpers
42
+ end
43
+ end
44
+ end
@@ -1,11 +1,11 @@
1
1
  module ViewSpec
2
2
  class Registry
3
3
  include Enumerable
4
-
5
- delegate :each, to: :@specs
4
+ attr_reader :specs
5
+ delegate_missing_to :@specs
6
6
 
7
7
  def initialize
8
- @specs = []
8
+ @specs = SpecCollection.new
9
9
  @source_files = {}
10
10
  end
11
11
 
@@ -21,34 +21,18 @@ module ViewSpec
21
21
  @source_files[path.to_s] ||= SourceFile.new(path)
22
22
  end
23
23
 
24
- def find(subject)
25
- find_where({subject:})
26
- end
27
-
28
- def has?(...)
29
- !!find(...)
30
- end
31
-
32
24
  def delete(subject)
33
25
  index = @specs.index { _1.subject == subject }
34
26
  @specs.delete_at(index) unless index.nil?
35
27
  end
36
28
 
37
29
  def clear!
30
+ @source_files.each do |key, file|
31
+ MethodSource.instance_variable_get(:@lines_for_file)&.delete(file.absolute_path.to_s)
32
+ end
33
+
38
34
  @specs.clear
39
35
  @source_files.clear
40
36
  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
37
  end
54
38
  end
@@ -4,13 +4,16 @@ module ViewSpec
4
4
  attr_accessor :short_identifier
5
5
  end
6
6
 
7
- def initialize(identifier, tpl = nil, &block)
7
+ delegate :source, :lang, to: :@executable
8
+
9
+ def initialize(identifier, executable, assigns: {})
8
10
  @identifier = identifier
9
- @block = tpl ? -> { render inline: tpl } : block
11
+ @executable = executable
12
+ @assigns = assigns
10
13
  end
11
14
 
12
15
  def render_in(view_context)
13
- render_context(view_context).instance_exec(&@block)&.strip&.html_safe
16
+ render_context(view_context).instance_exec(&@executable)&.strip&.html_safe
14
17
  end
15
18
 
16
19
  private
@@ -24,10 +27,8 @@ module ViewSpec
24
27
  view_context.instance_variable_set(:@current_template, current_template)
25
28
  end
26
29
 
27
- view_context.assigns.each do |key, value|
28
- view_context.define_singleton_method(key) do
29
- instance_variable_get(:"@#{key}") || {}
30
- end
30
+ view_context.assigns.merge(@assigns).each do |key, value|
31
+ view_context.define_singleton_method(key) { value }
31
32
  end
32
33
 
33
34
  view_context
@@ -1,58 +1,14 @@
1
+ require "redcarpet"
1
2
  module ViewSpec
2
- class Scenario < DSL::Context
3
- include DSL::Title
4
- include DSL::Params
5
- include DSL::Layout
6
- include DSL::Controller
3
+ class Scenario < Entry
4
+ def preview(**options)
5
+ raise NoPreviewError, "no preview defined" if context.preview.nil?
7
6
 
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
7
+ ScenarioPreview.new(self, context.preview, **options)
31
8
  end
32
9
 
33
10
  def to_param
34
11
  short_identifier
35
12
  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
13
  end
58
14
  end
@@ -0,0 +1,4 @@
1
+ module ViewSpec
2
+ class ScenarioCollection < EntryCollection
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ module ViewSpec
2
+ class ScenarioContext < Context
3
+ include DSL::Tests
4
+ include DSL::Title
5
+ include DSL::Notes
6
+ include DSL::Layout
7
+ include DSL::Params
8
+ include DSL::Preview
9
+ include DSL::Controller
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module ViewSpec
2
+ class ScenarioPreview < Preview
3
+ delegate :source, :lang, to: :renderable
4
+
5
+ def initialize(entry, executable, **options)
6
+ @executable = executable
7
+ super(entry, **options)
8
+ end
9
+
10
+ def call(view_context = nil)
11
+ @entry.validate_params(@params)
12
+ @rendered ||= renderer(view_context).render(renderable, layout:)
13
+ end
14
+
15
+ def raw
16
+ CGI.unescapeHTML(to_s)
17
+ end
18
+
19
+ alias_method :to_s, :call
20
+ alias_method :to_str, :call
21
+
22
+ private
23
+
24
+ attr_reader :layout
25
+
26
+ def renderable
27
+ @renderable ||= Renderable.new(@entry.short_identifier, @executable, assigns: @assigns.to_h.merge(params:))
28
+ end
29
+ end
30
+ end
@@ -30,6 +30,22 @@ module ViewSpec
30
30
  absolute_path.to_s.end_with?("#{ViewSpec.config.spec_file_suffix}.rb")
31
31
  end
32
32
 
33
+ def lang_from_line(lineno, fallback)
34
+ unless lineno.nil?
35
+ source_line = line(lineno)
36
+ if source_line.present?
37
+ tag = Utils.heredoc_tag(source_line)
38
+ return tag.downcase.to_sym if tag.present?
39
+ end
40
+ end
41
+
42
+ fallback
43
+ end
44
+
45
+ def line(lineno)
46
+ File.readlines(absolute_path)[lineno.to_i - 1]
47
+ end
48
+
33
49
  alias_method :to_path, :to_s
34
50
  alias_method :to_pathname, :absolute_path
35
51
 
@@ -51,5 +67,11 @@ module ViewSpec
51
67
  def path_segments
52
68
  absolute_path.to_s.split("/")
53
69
  end
70
+
71
+ def read_line(lineno)
72
+ File.open(absolute_path, "r").each_with_index do |line_str, i|
73
+ return line_str if i + 1 == lineno
74
+ end
75
+ end
54
76
  end
55
77
  end
@@ -1,11 +1,5 @@
1
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
-
2
+ class Spec < Entry
9
3
  attr_reader :subject, :source_file
10
4
 
11
5
  def initialize(source_file, subject, &block)
@@ -14,10 +8,6 @@ module ViewSpec
14
8
  super(subject, nil, &block)
15
9
  end
16
10
 
17
- def short_identifier
18
- @subject.to_short_identifier
19
- end
20
-
21
11
  def lookup_path
22
12
  if @source_file.spec_file?
23
13
  @source_file.lookup_path
@@ -30,8 +20,8 @@ module ViewSpec
30
20
  lookup_path
31
21
  end
32
22
 
33
- def to_partial_path
34
- "view_spec/spec"
23
+ def previews?
24
+ scenarios(flatten: true).select { _1.preview? }.any?
35
25
  end
36
26
  end
37
27
  end
@@ -0,0 +1,7 @@
1
+ module ViewSpec
2
+ class SpecCollection < EntryCollection
3
+ def find(subject = nil, &block)
4
+ subject.nil? ? items.find(&block) : find_where({subject:})
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module ViewSpec
2
+ class SpecContext < Context
3
+ include DSL::Title
4
+ include DSL::Notes
5
+ include DSL::Layout
6
+ include DSL::Groups
7
+ include DSL::Params
8
+ include DSL::Scenarios
9
+ include DSL::Controller
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module ViewSpec
2
+ class TestCase < ActiveSupport::TestCase
3
+ include TestHelpers
4
+
5
+ def render_preview(**params)
6
+ @rendered_content = Nokogiri::HTML.fragment(scenario.preview(params:).to_s)
7
+ @rendered_content
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ module ViewSpec
2
+ module TestHelpers
3
+ begin
4
+ require "capybara/minitest"
5
+ include Capybara::Minitest::Assertions
6
+
7
+ def page
8
+ @page ||= Capybara::Node::Simple.new(rendered_content)
9
+ end
10
+ rescue LoadError
11
+ if ENV["DEBUG"]
12
+ warn("ViewSpec::TestHelpers: Add `capybara` to Gemfile to use Capybara assertions.")
13
+ end
14
+ end
15
+
16
+ attr_reader :rendered_content
17
+
18
+ def render_preview(preview)
19
+ @rendered_content = Nokogiri::HTML.fragment(preview.to_s)
20
+ @rendered_content
21
+ end
22
+ end
23
+ end
@@ -2,7 +2,7 @@ module ViewSpec
2
2
  module Types
3
3
  class Symbol < Type
4
4
  def cast(value)
5
- value.to_sym
5
+ value&.to_sym
6
6
  end
7
7
  end
8
8
  end
@@ -4,6 +4,47 @@ module ViewSpec
4
4
  paths.map { File.absolute_path(_1, Rails.root) }
5
5
  end
6
6
 
7
- module_function :normalize_paths
7
+ def create_namespace(ns, root = Object)
8
+ module_names = ns.split("::")
9
+ current_context = root
10
+ module_names.each do |module_name|
11
+ unless module_name == "ViewSpecs"
12
+ current_context.const_set(module_name, Module.new)
13
+ end
14
+ rescue
15
+ ensure
16
+ current_context = current_context.const_get(module_name)
17
+ end
18
+ current_context
19
+ end
20
+
21
+ def create_class(name, root = Object, base_class: nil, &block)
22
+ namespace_parts = name.camelize.split("::")
23
+ class_name = namespace_parts.pop
24
+ namespace = namespace_parts.join("::")
25
+
26
+ scope = create_namespace(namespace, root)
27
+ scope.const_set(class_name, Class.new(base_class))
28
+
29
+ klass = scope.const_get(class_name)
30
+ klass.instance_exec(&block) if block
31
+ klass
32
+ end
33
+
34
+ def heredoc_tag(source_line)
35
+ matches = source_line.match(/<<~?(?<tag>[A-Z_\d]+)/)
36
+ matches[:tag] if matches
37
+ end
38
+
39
+ def lang_from_source_line(source_line, fallback = nil)
40
+ if source_line.present?
41
+ tag = Utils.heredoc_tag(source_line)
42
+ tag.present? ? tag.downcase.to_sym : fallback
43
+ else
44
+ fallback
45
+ end
46
+ end
47
+
48
+ module_function :normalize_paths, :create_namespace, :create_class, :heredoc_tag
8
49
  end
9
50
  end
@@ -1,3 +1,3 @@
1
1
  module ViewSpec
2
- VERSION = "0.0.0"
2
+ VERSION = "0.0.1"
3
3
  end
data/lib/view_spec.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "zeitwerk"
2
2
  require "active_support"
3
+ require "method_source"
3
4
  require "view_spec/version"
4
5
 
5
6
  loader = Zeitwerk::Loader.for_gem
@@ -11,8 +12,6 @@ loader.enable_reloading if ENV["RAILS_ENV"] == "development"
11
12
  loader.setup
12
13
 
13
14
  module ViewSpec
14
- include DSL
15
-
16
15
  class << self
17
16
  def config
18
17
  @config ||= Config.instance
@@ -22,6 +21,19 @@ module ViewSpec
22
21
  yield config
23
22
  end
24
23
 
24
+ def register(subject, &block)
25
+ source_path = caller_locations(1..1).first.absolute_path
26
+ registry.add(source_path, subject, &block)
27
+ end
28
+
29
+ def registry
30
+ @registry ||= Registry.new
31
+ end
32
+
33
+ def specs
34
+ registry.specs
35
+ end
36
+
25
37
  def load!
26
38
  registry.clear!
27
39
 
@@ -32,6 +44,8 @@ module ViewSpec
32
44
 
33
45
  Utils.normalize_paths(config.spec_files).each { load _1 }
34
46
  end
47
+
48
+ alias_method :spec, :register
35
49
  end
36
50
  end
37
51