minitest_shopify_themes 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c93992986e47e83e7d81af19832e5fbdd05281444dc20992a7bf5fcce9e0886b
4
+ data.tar.gz: 646dcc863bc5ceebae020eaac691d2eebca236aedb120665fb0bd41c6127a695
5
+ SHA512:
6
+ metadata.gz: a501321c5e52ed4d1efa4e8aa5635069a24d8a8694ebcb86a11eb688d58704bbe701c282e6fa65dd0b14a43606a167dbaf417c2743fc0fc6340614e7ada43a88
7
+ data.tar.gz: e32d14b57184b516fa70340d42f142903a79b5cbcac6343ff438878a314d330ead7b7a2a88662b7c65ba4e5eb21fe12336d6d3e3c3f18040b995e0c8de84e1bb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Minitest Shopify
2
+
3
+ Prototype of a minitest test class with capybara assertions capable of rendering sections and snippets of a liquid based Shopify theme.
4
+
5
+ ## Example
6
+ ```ruby
7
+ MinitestShopifyThemes.configure do |config|
8
+ config.theme_root = File.join(__dir__, "theme")
9
+ config.selenium_driver = :selenium_chrome_headless
10
+ config.layout_file = "layout/theme"
11
+ end
12
+ ```
13
+
14
+ ```ruby
15
+ require "minitest/autorun"
16
+ require "minitest_shopify_themes"
17
+
18
+ class TestCard < MinitestShopifyThemes::LiquidTest
19
+ def test_renders_a_card
20
+ render template: "snippets/card", variables: { comment: default_comment }
21
+ assert_text "Hello world!"
22
+ assert_text "John Doe"
23
+ end
24
+
25
+ private
26
+
27
+ def default_comment
28
+ {
29
+ id: 1,
30
+ created_at: "2023-07-20T19:31:35Z",
31
+ content: "Hello world!",
32
+ author: {
33
+ id: 1,
34
+ name: "John Doe"
35
+ }
36
+ }
37
+ end
38
+ end
39
+ ```
40
+
41
+ You can also use selenium to run tests that involve JavaScript or assets:
42
+
43
+ ```ruby
44
+ require "minitest/autorun"
45
+ require "minitest_shopify_themes"
46
+
47
+ class TestCardView < MinitestShopifyThemes::ViewTest
48
+ def test_javascript_enabled_card
49
+ render template: "snippets/js-card", variables: { comment: default_comment }
50
+ within "#comment-1" do
51
+ assert_text "Javascript is enabled"
52
+ assert_no_text "Hello World"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def default_comment
59
+ {
60
+ id: 1,
61
+ content: "Hello world!",
62
+ }
63
+ end
64
+ end
65
+ ```
66
+
67
+ ## Installation
68
+ This is an experiment for the time being, if you want to try it out for yourself you can add the following definition to your gemfile:
69
+
70
+ ```ruby
71
+ gem "minitest_shopify_themes", git: "https://github.com/nebulab/minitest_shopify_themes.git", branch: "main"
72
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test)
7
+ task default: :test
@@ -0,0 +1,28 @@
1
+ require "pathname"
2
+ require "liquid"
3
+
4
+ class MinitestShopifyThemes::Configuration
5
+ attr_reader :theme_root
6
+ attr_accessor :selenium_driver, :layout_file
7
+
8
+ def initialize
9
+ self.theme_root = "./theme"
10
+ @selenium_driver = :selenium_chrome_headless
11
+ @layout_file = nil
12
+ end
13
+
14
+ def theme_root=(value)
15
+ @theme_root = Pathname(value).expand_path
16
+
17
+ # We set up a file system to be able to render snippets from within
18
+ # the templates we provide to the render method. The render method is
19
+ # only used to render a snippet or app block, however, we cannot
20
+ # emulate the render of an app block, so its okay to default to
21
+ # snippets.
22
+ Liquid::Environment.default.file_system = MinitestShopifyThemes::LocalFileSystem.new(@theme_root.to_s, "snippets/%s.liquid")
23
+ end
24
+
25
+ def assets_dir
26
+ @theme_root&.join("assets")
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ module MinitestShopifyThemes::Filters::AssetsFilter
2
+ def asset_url(input)
3
+ File.join("/assets", input)
4
+ end
5
+
6
+ def script_tag(input)
7
+ "<script src=\"#{input}\" type=\"text/javascript\"></script>"
8
+ end
9
+
10
+ def stylesheet_tag(input)
11
+ "<link href=\"#{input}\" rel=\"stylesheet\" type=\"text/css\" media=\"all\" />"
12
+ end
13
+
14
+ Liquid::Environment.default.register_filter(self)
15
+ end
@@ -0,0 +1,12 @@
1
+ module MinitestShopifyThemes::Filters::ImageFilter
2
+ def image_url(input, width = nil, height = nil)
3
+ input
4
+ end
5
+
6
+ def image_tag(input, args = [])
7
+ attributes = args.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
8
+ "<img src=\"#{input}\" #{attributes} />"
9
+ end
10
+
11
+ Liquid::Environment.default.register_filter(self)
12
+ end
@@ -0,0 +1,29 @@
1
+ require 'money'
2
+
3
+ module MinitestShopifyThemes::Filters::MoneyFilter
4
+ module TestHelper
5
+ def setup
6
+ super
7
+ Money.locale_backend = :i18n
8
+ end
9
+ end
10
+
11
+ class ShopifyMoney < Money
12
+ self.rounding_mode = BigDecimal::ROUND_HALF_UP
13
+ self.locale_backend = :i18n
14
+
15
+ def to_liquid
16
+ to_json
17
+ end
18
+
19
+ def to_liquid_value
20
+ to_html
21
+ end
22
+ end
23
+
24
+ def money(input, currency: "USD")
25
+ ShopifyMoney.new(input, currency)
26
+ end
27
+
28
+ Liquid::Environment.default.register_filter(self)
29
+ end
@@ -0,0 +1,46 @@
1
+ require 'i18n'
2
+
3
+ module MinitestShopifyThemes::Filters::TranslationsFilter
4
+ module TestHelper
5
+ def setup
6
+ super
7
+
8
+ Dir["#{MinitestShopifyThemes.configuration.theme_root}/locales/*.json"].each do |file|
9
+ next if file.end_with?('.schema.json')
10
+
11
+ command = %{node -pe "JSON.stringify(eval('data='+require('fs').readFileSync(0, 'utf8')))"}
12
+ # Use Node.js to parse JSON, as it handles comments in JSON files
13
+ # and is more lenient than Ruby's JSON parser.
14
+ r, w = IO.pipe
15
+ pid = Process.spawn(command, in: file, out: w, err: :close, close_others: true)
16
+ w.close
17
+ Process.wait(pid)
18
+ raise "Failed to parse JSON from #{file}" unless $?.success?
19
+ contents = r.read
20
+ r.close
21
+ require 'json'
22
+ data = JSON.parse!(contents) rescue binding.irb
23
+ locale = File.basename(file, '.json').chomp(".default")
24
+ I18n.backend.store_translations(locale.to_sym, data)
25
+ if file.end_with?('.default.json')
26
+ @default_locale = locale.to_sym
27
+ end
28
+ end
29
+
30
+ I18n.locale = @default_locale || I18n.default_locale
31
+ end
32
+ end
33
+
34
+ def t(key, variables = {})
35
+ I18n.t(key).gsub(/{{\s*([a-zA-Z0-9_]+)\s*}}/) do |match|
36
+ var_name = $1
37
+ if variables.key?(var_name)
38
+ variables[var_name].to_s
39
+ else
40
+ match # Return the original match if variable not found
41
+ end
42
+ end
43
+ end
44
+
45
+ Liquid::Environment.default.register_filter(self)
46
+ end
@@ -0,0 +1,51 @@
1
+ require "minitest/autorun"
2
+ require "capybara/minitest"
3
+ require "liquid"
4
+ require "loofah"
5
+
6
+ MinitestShopifyThemes.loader.eager_load_namespace(MinitestShopifyThemes::Tags)
7
+ MinitestShopifyThemes.loader.eager_load_namespace(MinitestShopifyThemes::Filters)
8
+
9
+ class MinitestShopifyThemes::LiquidTest < Minitest::Test
10
+ include Capybara::Minitest::Assertions
11
+ include MinitestShopifyThemes::Filters::TranslationsFilter::TestHelper
12
+ include MinitestShopifyThemes::Filters::MoneyFilter::TestHelper
13
+
14
+ def render(template:, variables: {})
15
+ @page = render_liquid(template:, variables:)
16
+
17
+ if MinitestShopifyThemes.configuration.layout_file
18
+ @page = render_liquid(
19
+ template: MinitestShopifyThemes.configuration.layout_file,
20
+ variables: variables.merge({ content_for_layout: @page })
21
+ )
22
+ end
23
+ end
24
+
25
+ # Helper to enable Capybara assertions on the rendered template.
26
+ def page
27
+ Capybara.string(@page)
28
+ end
29
+
30
+ def render_liquid(template:, variables:)
31
+ file = File.read(File.join(MinitestShopifyThemes.configuration.theme_root, template) + ".liquid")
32
+ template = Liquid::Template.parse(file, error_mode: :strict)
33
+ @output = template.render!(deep_stringify_keys(variables), {strict_variables: true, strict_filters: true})
34
+ end
35
+
36
+ def text
37
+ html&.to_text(encode_special_chars: false).gsub(/\s+/, ' ').strip
38
+ end
39
+
40
+ def html
41
+ Loofah.html5_fragment(@output)
42
+ end
43
+
44
+ def deep_stringify_keys(hash)
45
+ # Liquid expects string keys for variables, but Rails convention is to use symbols.
46
+ # This method recursively converts all keys to strings so either convention can be used.
47
+ hash.each_with_object({}) do |(key, value), acc|
48
+ acc[key.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,24 @@
1
+ require "liquid"
2
+
3
+ module MinitestShopifyThemes
4
+ # This is an override for the LocalFileSystem that is part of Liquid
5
+ # gem. However, out of the box it does not support snippets with a
6
+ # '-' in the name. This override exists to allow for that and achieve
7
+ # parity with Shopify. We might want to push this upstream to the
8
+ # Liquid gem.
9
+ class LocalFileSystem < Liquid::LocalFileSystem
10
+ def full_path(template_path)
11
+ raise ::Liquid::FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_\-/]+\z}.match?(template_path)
12
+
13
+ full_path = if template_path.include?('/')
14
+ File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
15
+ else
16
+ File.join(root, @pattern % template_path)
17
+ end
18
+
19
+ raise ::Liquid::FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
20
+
21
+ full_path
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,8 @@
1
+ class MinitestShopifyThemes::Tags::SchemaTag < Liquid::Block
2
+ def render(context)
3
+ # The schema tag renders nothing.
4
+ nil
5
+ end
6
+
7
+ Liquid::Environment.default.register_tag('schema', self)
8
+ end
@@ -0,0 +1,13 @@
1
+ class MinitestShopifyThemes::Tags::SectionTag < Liquid::Tag
2
+ def initialize(tag_name, section_name, options)
3
+ @section_name = section_name.strip.gsub("'", "")
4
+ end
5
+
6
+ def render(context)
7
+ file = MinitestShopifyThemes.configuration.theme_root.join("sections", @section_name + ".liquid").read
8
+ template = Liquid::Template.parse(file, error_mode: :strict)
9
+ template.render!
10
+ end
11
+
12
+ Liquid::Environment.default.register_tag('section', self)
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MinitestShopifyThemes
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,37 @@
1
+ class MinitestShopifyThemes::ViewTest < MinitestShopifyThemes::LiquidTest
2
+ include Capybara::DSL
3
+
4
+ VIEWS_DIR = Dir.mktmpdir
5
+ Minitest.after_run { FileUtils.remove_entry VIEWS_DIR }
6
+ Capybara.server = :puma, { Silent: true }
7
+
8
+ def setup
9
+ Capybara.current_driver = MinitestShopifyThemes.configuration.selenium_driver
10
+ Capybara.app ||= Rack::Lint.new Rack::Cascade.new [
11
+ Rack::Files.new(VIEWS_DIR),
12
+ Rack::URLMap.new("/assets" => Rack::Files.new(MinitestShopifyThemes.configuration.assets_dir)),
13
+ ]
14
+ end
15
+
16
+ def teardown
17
+ Capybara.reset_sessions!
18
+ Capybara.use_default_driver
19
+ end
20
+
21
+ def render(**)
22
+ super
23
+ host_page do |path|
24
+ visit path
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def host_page
31
+ Tempfile.create(['', '.html'], VIEWS_DIR) do |f|
32
+ f.write(@page)
33
+ f.flush
34
+ yield Pathname.new(f.path).basename
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "minitest_shopify_themes/version"
2
+ require "zeitwerk"
3
+
4
+ module MinitestShopifyThemes
5
+ def self.loader
6
+ @loader ||= Zeitwerk::Loader.for_gem
7
+ end
8
+
9
+ loader.setup
10
+ # loader.eager_load # optionally
11
+
12
+ def self.configuration
13
+ @configuration ||= Configuration.new
14
+ end
15
+
16
+ def self.configure
17
+ yield configuration
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest_shopify_themes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Belzer
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-06-11 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: capybara
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: selenium-webdriver
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: puma
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rackup
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: liquid
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: bigdecimal
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: zeitwerk
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: i18n
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: money
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: loofah
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ email: nickbelzer@nebulab.com
167
+ executables: []
168
+ extensions: []
169
+ extra_rdoc_files: []
170
+ files:
171
+ - ".ruby-version"
172
+ - README.md
173
+ - Rakefile
174
+ - lib/minitest_shopify_themes.rb
175
+ - lib/minitest_shopify_themes/configuration.rb
176
+ - lib/minitest_shopify_themes/filters/assets_filter.rb
177
+ - lib/minitest_shopify_themes/filters/image_filter.rb
178
+ - lib/minitest_shopify_themes/filters/money_filter.rb
179
+ - lib/minitest_shopify_themes/filters/translations_filter.rb
180
+ - lib/minitest_shopify_themes/liquid_test.rb
181
+ - lib/minitest_shopify_themes/local_file_system.rb
182
+ - lib/minitest_shopify_themes/tags/schema_tag.rb
183
+ - lib/minitest_shopify_themes/tags/section_tag.rb
184
+ - lib/minitest_shopify_themes/version.rb
185
+ - lib/minitest_shopify_themes/view_test.rb
186
+ homepage: https://github.com/nebulab/minitest-shopify?tab=readme-ov-file#readme
187
+ licenses:
188
+ - MIT
189
+ metadata:
190
+ allowed_push_host: https://rubygems.org
191
+ bug_tracker_uri: https://github.com/nebulab/minitest-shopify/issues
192
+ homepage_uri: https://github.com/nebulab/minitest-shopify?tab=readme-ov-file#readme
193
+ source_code_uri: https://github.com/nebulab/minitest-shopify
194
+ changelog_uri: https://github.com/nebulab/minitest-shopify/releases
195
+ rdoc_options: []
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: 3.1.0
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ requirements: []
209
+ rubygems_version: 3.6.2
210
+ specification_version: 4
211
+ summary: A minitest plugin for testing Shopify Liquid themes
212
+ test_files: []