brut 0.0.13 → 0.0.21
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 +4 -4
- data/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +9 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +9 -46
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +63 -2
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +101 -246
- data/lib/brut/front_end/components/constraint_violations.rb +10 -10
- data/lib/brut/front_end/components/form_tag.rb +17 -29
- data/lib/brut/front_end/components/i18n_translations.rb +12 -13
- data/lib/brut/front_end/components/input.rb +0 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
- data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
- data/lib/brut/front_end/components/inputs/select.rb +13 -20
- data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
- data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
- data/lib/brut/front_end/components/locale_detection.rb +2 -2
- data/lib/brut/front_end/components/page_identifier.rb +3 -5
- data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
- data/lib/brut/front_end/components/traceparent.rb +5 -6
- data/lib/brut/front_end/http_method.rb +4 -0
- data/lib/brut/front_end/inline_svg_locator.rb +21 -0
- data/lib/brut/front_end/layout.rb +19 -0
- data/lib/brut/front_end/page.rb +52 -40
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +8 -3
- data/lib/brut/front_end.rb +32 -0
- data/lib/brut/i18n/base_methods.rb +51 -11
- data/lib/brut/i18n/for_back_end.rb +8 -0
- data/lib/brut/i18n/for_cli.rb +5 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +13 -7
- data/lib/brut/spec_support/component_support.rb +27 -13
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +5 -4
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +7 -50
- metadata +14 -49
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/template.rb +0 -47
- data/lib/brut/front_end/templates/block_filter.rb +0 -61
- data/lib/brut/front_end/templates/erb_engine.rb +0 -26
- data/lib/brut/front_end/templates/erb_parser.rb +0 -84
- data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
- data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
- data/lib/brut/front_end/templates/locator.rb +0 -60
@@ -1,6 +1,11 @@
|
|
1
|
-
# Interface for translations
|
2
|
-
#
|
3
|
-
#
|
1
|
+
# Interface for translations, preferred over Ruby's I18n classes. Note that this is a
|
2
|
+
# base module and not intended to be directly used in your classes. Include one of
|
3
|
+
# the other modules in this namespace:
|
4
|
+
#
|
5
|
+
# * {Brut::I18n::ForHTML} for components or pages, or anything use Phlex
|
6
|
+
# * {Brut::I18n::ForCLI} for CLI apps
|
7
|
+
# * {Brut::I18n::ForBackEnd} for back-end classes that aren't generating HTML
|
8
|
+
#
|
4
9
|
module Brut::I18n::BaseMethods
|
5
10
|
|
6
11
|
# Access a translation and insert interpolated elemens as needed. This will use the provided key to determine
|
@@ -18,12 +23,12 @@ module Brut::I18n::BaseMethods
|
|
18
23
|
# to `t("email.required", field: "E-mail address")` would generate `"E-mail address is required"`.
|
19
24
|
#
|
20
25
|
# @param [String,Symbol,Array<String>,Array<Symbol>] key used to create one or more keys to be translated.
|
21
|
-
# This value's behavior is designed to
|
26
|
+
# This value's behavior is designed to balance predictabilitiy in what actual key is chosen
|
22
27
|
# but without needless repetition on a page. If this value is provided, and is an array, the values
|
23
28
|
# are joined with "." to form a key. If the value is not an array, that value is used directly.
|
24
29
|
# Given this key, two values are checked for a translation: the key itself and
|
25
30
|
# the key inside "general.". If this value is *not* provided, it is expected
|
26
|
-
#
|
31
|
+
# that the `**rest` hash includes page: or component:. See that parameter and the example.
|
27
32
|
#
|
28
33
|
# @param [Hash] rest values to use for interpolation of the key's translation. If `key` is omitted,
|
29
34
|
# this hash should have a value for either `page:` or `component:` (not both). If
|
@@ -34,6 +39,16 @@ module Brut::I18n::BaseMethods
|
|
34
39
|
# Note that if the page– or component–specific key is not found, this will check
|
35
40
|
# `general.«page: value»`.
|
36
41
|
# @option interpolated_values [Numeric] count Special interpolation to control pluralization.
|
42
|
+
# @yield Nothing is yielded if a block is given, however the value returned is used for the `%{block}`
|
43
|
+
# interpolation value.
|
44
|
+
# @yieldreturn [String] The value to use for the `%{block}` interpolation value. There is some nuance to
|
45
|
+
# how this works. The value returned is given to `capture`, and that value
|
46
|
+
# is given to `safe`. Outside of an HTML-rendering context, these methods
|
47
|
+
# simply pass through the contents of the block. In an HTML-rendering
|
48
|
+
# context, however, these methods are assumed to be from
|
49
|
+
# [`Phlex::HTML`](https://phlex.fun). `capture` will create a new Phlex
|
50
|
+
# context and capture any HTML built inside the block. That HTML is assumed
|
51
|
+
# to be safe, thus `safe` is called to communicate this to Phlex.
|
37
52
|
#
|
38
53
|
# @raise [I18n::MissingTranslation] if no translation is found
|
39
54
|
# @raise [I18n::MissingInterpolationArgument] if interpolation arguments are missing, or if the key
|
@@ -84,7 +99,7 @@ module Brut::I18n::BaseMethods
|
|
84
99
|
# t(page: :new_widget) # => Create new Widget
|
85
100
|
# # in your code for WidgetsPage
|
86
101
|
# t(page: :new_widget) # => Create New
|
87
|
-
# # in your code for
|
102
|
+
# # in your code for SomeOtherPage
|
88
103
|
# t(page: :new_widget) # => Make a New Widget
|
89
104
|
#
|
90
105
|
# @example Using page: with an array
|
@@ -101,7 +116,31 @@ module Brut::I18n::BaseMethods
|
|
101
116
|
# }
|
102
117
|
# # in your code for HomePage
|
103
118
|
# t(page: [ :captions, :new ]) # => New Widgets
|
104
|
-
|
119
|
+
#
|
120
|
+
# @example Using a block with Phlex
|
121
|
+
# # in your translations file
|
122
|
+
# en: {
|
123
|
+
# greeting: "Hello there %{name}, you may %{block}",
|
124
|
+
# }
|
125
|
+
# # Inside a component where
|
126
|
+
# # Brut::I18n::ForHTML has been included
|
127
|
+
# def view_template
|
128
|
+
# h1 do
|
129
|
+
# raw(t(:greeting), name: user.name) do
|
130
|
+
# a(href: "https://support.example.com") do
|
131
|
+
# "contact support"
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
# end
|
135
|
+
# end
|
136
|
+
# # This will produce this HTML, assuming user.name is "Pat":
|
137
|
+
# <h1>
|
138
|
+
# Hell there Pat, you may
|
139
|
+
# <a href="https://support.example.com">
|
140
|
+
# contact support
|
141
|
+
# </a>
|
142
|
+
# </h1>
|
143
|
+
def t(key=:look_in_rest,**rest,&block)
|
105
144
|
if key == :look_in_rest
|
106
145
|
|
107
146
|
page = rest.delete(:page)
|
@@ -126,13 +165,14 @@ module Brut::I18n::BaseMethods
|
|
126
165
|
key = Array(key).join('.')
|
127
166
|
key = [key,"general.#{key}"]
|
128
167
|
end
|
129
|
-
if
|
168
|
+
if !block.nil?
|
130
169
|
if rest[:block]
|
131
170
|
raise ArgumentError,"t was given a block and a block: param. You can't do both "
|
132
171
|
end
|
133
|
-
|
172
|
+
block_contents = safe(capture(&block))
|
173
|
+
rest[:block] = block_contents
|
134
174
|
end
|
135
|
-
|
175
|
+
t_direct(key,**rest)
|
136
176
|
rescue I18n::MissingInterpolationArgument => ex
|
137
177
|
if ex.key.to_s == "block"
|
138
178
|
raise ArgumentError,"One of the keys #{key.join(", ")} contained a %{block} interpolation value: '#{ex.string}'. This means you must use t_html *and* yield a block to it"
|
@@ -168,7 +208,7 @@ module Brut::I18n::BaseMethods
|
|
168
208
|
}
|
169
209
|
escaped_interpolated_values = interpolated_values.map { |key,value|
|
170
210
|
if value.kind_of?(String)
|
171
|
-
[ key,
|
211
|
+
[ key, CGI.escapeHTML(value) ]
|
172
212
|
else
|
173
213
|
[ key, value ]
|
174
214
|
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# Use this to access translations in any back-end code.
|
2
|
+
# This implementation does support blocks yielded to {#t}, however
|
3
|
+
# their values are not necessarily HTML-escaped.
|
4
|
+
module Brut::I18n::ForBackEnd
|
5
|
+
include Brut::I18n::BaseMethods
|
6
|
+
def safe(string) = string
|
7
|
+
def capture(&block) = block.()
|
8
|
+
end
|
data/lib/brut/i18n/for_cli.rb
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
+
# Use this to access translations in any CLI.
|
2
|
+
# This implementation does support blocks yielded to {#t}, however
|
3
|
+
# their values are not necessarily HTML-escaped.
|
1
4
|
module Brut::I18n::ForCLI
|
2
5
|
include Brut::I18n::BaseMethods
|
3
|
-
def
|
6
|
+
def safe(string) = string
|
7
|
+
def capture(&block) = block.()
|
4
8
|
end
|
data/lib/brut/i18n/for_html.rb
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
+
# I18n for components or pages, which are assumed to be Phlex components.
|
2
|
+
# To use this outside of a Phlex context, you must define these two
|
3
|
+
# methods to ensure proper HTML escaping happens:
|
4
|
+
#
|
5
|
+
# * `safe` to accept a string and return a string.
|
6
|
+
# * `capture` to accept a block and return its contents as a string.
|
1
7
|
module Brut::I18n::ForHTML
|
2
8
|
include Brut::I18n::BaseMethods
|
3
|
-
def
|
9
|
+
def t(...)
|
10
|
+
safe(super)
|
11
|
+
end
|
4
12
|
end
|
@@ -1,8 +1,19 @@
|
|
1
|
+
# Manages the value for the HTTP
|
2
|
+
# [Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Language)
|
3
|
+
# header. Generally, you would not interact with this class directly, however it is used
|
4
|
+
# by Brut to make a guess as to which Locale a browser is reporting.
|
1
5
|
class Brut::I18n::HTTPAcceptLanguage
|
6
|
+
# A locale with the weight (value for q=) it was given in the Accept-Language header
|
2
7
|
WeightedLocale = Data.define(:locale, :q) do
|
8
|
+
# Returns the primary locale for whatever locale
|
9
|
+
# this is holding. For example, the primary locale
|
10
|
+
# of "en-US" is "en".
|
3
11
|
def primary_locale = self.locale.gsub(/\-.*$/,"")
|
12
|
+
|
13
|
+
# True if this locale is a primary locale
|
4
14
|
def primary? = self.primary_locale == self.locale
|
5
15
|
|
16
|
+
# Return a new WeightedLocale that is the primary locale.
|
6
17
|
def primary_only
|
7
18
|
self.class.new(locale: self.primary_locale, q: self.q)
|
8
19
|
end
|
@@ -12,6 +23,11 @@ class Brut::I18n::HTTPAcceptLanguage
|
|
12
23
|
end
|
13
24
|
end
|
14
25
|
|
26
|
+
# Parse the value stored in the session.
|
27
|
+
#
|
28
|
+
# @param [String] session_value the value stored in the session.
|
29
|
+
# @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
|
30
|
+
# is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
|
15
31
|
def self.from_session(session_value)
|
16
32
|
values = session_value.to_s.split(/,/).map { |value|
|
17
33
|
locale,q = value.split(/;/)
|
@@ -24,6 +40,19 @@ class Brut::I18n::HTTPAcceptLanguage
|
|
24
40
|
end
|
25
41
|
end
|
26
42
|
|
43
|
+
# Parse the value provided by the browser via
|
44
|
+
# {Brut::FrontEnd::Handlers::LocaleDetectionHandler} via
|
45
|
+
# the `brut-locale-detection` custom element (which
|
46
|
+
# uses `Intl.DateTimeFormat().resolvedOptions()` to determine
|
47
|
+
# the locale).
|
48
|
+
#
|
49
|
+
# Because this value is not in the same format as the Accept-Language
|
50
|
+
# header, it's `q` is assumed to be 1.
|
51
|
+
#
|
52
|
+
# @param [String] value the value provided by the brower.
|
53
|
+
#
|
54
|
+
# @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
|
55
|
+
# is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
|
27
56
|
def self.from_browser(value)
|
28
57
|
value = value.to_s.strip
|
29
58
|
if value == ""
|
@@ -33,6 +62,10 @@ class Brut::I18n::HTTPAcceptLanguage
|
|
33
62
|
end
|
34
63
|
end
|
35
64
|
|
65
|
+
# Parse from the HTTP Accept-Language header.
|
66
|
+
#
|
67
|
+
# @return [Brut::I18n::HTTPAcceptLanguage] a usable object. If the provided value
|
68
|
+
# is blank, #{Brut::I18n::HTTPAcceptLanguage::AlwaysEnglish} is returned.
|
36
69
|
def self.from_header(header_value)
|
37
70
|
header_value = header_value.to_s.strip
|
38
71
|
if header_value == "*" || header_value == ""
|
@@ -50,14 +83,28 @@ class Brut::I18n::HTTPAcceptLanguage
|
|
50
83
|
end
|
51
84
|
end
|
52
85
|
|
86
|
+
# Ordered list of locales, from highest-weighted to lowest.
|
53
87
|
attr_reader :weighted_locales
|
88
|
+
# @param [Array<Brut::I18n::HTTPAcceptLanguage::WeightedLocale>] weighted_locales locales to use. They do not
|
89
|
+
# need to be ordered
|
54
90
|
def initialize(weighted_locales)
|
55
91
|
@weighted_locales = weighted_locales.sort_by(&:q).reverse
|
56
92
|
end
|
93
|
+
|
94
|
+
# True if the values inside this object represent known locales, and not a guess based on missing information.
|
95
|
+
# In general, this returns true if the values came from the Accept-Language header, or from the browser.
|
57
96
|
def known? = true
|
97
|
+
|
98
|
+
# Serialize for storage in the session
|
99
|
+
#
|
100
|
+
# @return [String] a string that can be stored in the session and later deserialized via {.from_session}.
|
58
101
|
def for_session = @weighted_locales.map { |weighted_locale| "#{weighted_locale.locale};#{weighted_locale.q}" }.join(",")
|
59
102
|
def to_s = self.for_session
|
60
103
|
|
104
|
+
# A subclass that represents the use of English and only English. This is
|
105
|
+
# used when attempts to determine the locale fail. Instances of this class
|
106
|
+
# are considered "unknown" ({#known?} returns false), which allows Brut
|
107
|
+
# to replace this with a known value later on.
|
61
108
|
class AlwaysEnglish < Brut::I18n::HTTPAcceptLanguage
|
62
109
|
def initialize
|
63
110
|
super([ WeightedLocale.new(locale: "en", q: 1) ])
|
data/lib/brut/i18n.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# I18n holds all the code useful for translating and localizing information. It's based on Ruby's I18n.
|
2
2
|
module Brut::I18n
|
3
3
|
autoload(:BaseMethods, "brut/i18n/base_methods")
|
4
|
+
autoload(:ForBackEnd, "brut/i18n/for_back_end")
|
4
5
|
autoload(:ForCLI, "brut/i18n/for_cli")
|
5
6
|
autoload(:ForHTML, "brut/i18n/for_html")
|
6
7
|
autoload(:HTTPAcceptLanguage, "brut/i18n/http_accept_language")
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# Class to interact with the OpenTelemetry standard in a simpler way than
|
2
|
+
# the provided Ruby gem does. In general, you should use this class
|
3
|
+
# via `Brut.container.instrumentation`, and you should *not* use the
|
4
|
+
# OpenTelemetry ruby library directly. You probably wouldn't want to, anyway.
|
1
5
|
class Brut::Instrumentation::OpenTelemetry
|
2
6
|
# Create a span around the given block of code.
|
3
7
|
#
|
@@ -39,11 +43,32 @@ class Brut::Instrumentation::OpenTelemetry
|
|
39
43
|
timestamp:)
|
40
44
|
end
|
41
45
|
|
46
|
+
# Record an exception. In general, use this only if:
|
47
|
+
#
|
48
|
+
# * You need to have the parent span record this particular exception
|
49
|
+
# * You are not going to re-raise the exception.
|
50
|
+
#
|
51
|
+
# Otherwise, look at {#record_and_reraise_exception!}.
|
52
|
+
#
|
53
|
+
# @param [Exception] ex the exception to record.
|
54
|
+
# @param [Hash] attributes any attributes to attach that will show up in your OTel provider
|
42
55
|
def record_exception(ex,attributes=nil)
|
43
56
|
current_span = OpenTelemetry::Trace.current_span
|
44
57
|
current_span.record_exception(ex,attributes: NormalizedAttributes.new(nil,attributes).to_h)
|
45
58
|
end
|
46
59
|
|
60
|
+
# Record an exception and re-raise it. This is useful if you want
|
61
|
+
# the exception recorded as part of the parent span, but still plan
|
62
|
+
# to let it raise. Don't do this for every exception you intend to raise.
|
63
|
+
# @param [Exception] ex the exception to record.
|
64
|
+
# @param [Hash] attributes any attributes to attach that will show up in your OTel provider
|
65
|
+
# @raise [Exception] the exception passed in.
|
66
|
+
def record_and_reraise_exception(ex,attributes=nil)
|
67
|
+
reecord_exception(ex,attributes)
|
68
|
+
raise ex
|
69
|
+
end
|
70
|
+
|
71
|
+
|
47
72
|
# Adds attributes to the span, converting the hash or keyword arguments to strings. This will use
|
48
73
|
# the app's Otel prefix for all attributes, so you do not have to prefix them.
|
49
74
|
# If you need to set standard attributes, you should use {Brut::Instrumentation::OpenTelemetry::Span#add_prefixed_attributes} instead.
|
data/lib/brut/instrumentation.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
+
# Namespace for instrumentation setup and support. Brut strives to provide useful
|
2
|
+
# instrumentation by default.
|
3
|
+
#
|
1
4
|
module Brut::Instrumentation
|
2
5
|
autoload(:OpenTelemetry,"brut/instrumentation/open_telemetry")
|
3
6
|
autoload(:LoggerSpanExporter,"brut/instrumentation/logger_span_exporter")
|
4
|
-
|
5
|
-
# Convenience method to add attributes to create a span without accessing the instrumentation instance directly.
|
6
|
-
def span(name,**attributes,&block)
|
7
|
-
Brut.container.instrumentation.span(name,**attributes,&block)
|
8
|
-
end
|
9
7
|
end
|
10
8
|
|
data/lib/brut/sinatra_helpers.rb
CHANGED
@@ -9,6 +9,7 @@ module Brut::SinatraHelpers
|
|
9
9
|
sinatra_app.path("/__brut/locale_detection",method: :post)
|
10
10
|
sinatra_app.path("/__brut/instrumentation",method: :get)
|
11
11
|
sinatra_app.set :host_authorization, permitted_hosts: Brut.container.permitted_hosts
|
12
|
+
sinatra_app.set :show_exceptions, false
|
12
13
|
end
|
13
14
|
|
14
15
|
# @private
|
@@ -77,15 +78,16 @@ module Brut::SinatraHelpers
|
|
77
78
|
|
78
79
|
Brut.container.instrumentation.span(page_class.name) do |span|
|
79
80
|
span.add_prefixed_attributes("brut", type: :page, class: page_class)
|
80
|
-
|
81
|
-
constructor_args = request_context.as_constructor_args(
|
81
|
+
constructor_args = Brut::FrontEnd::RequestContext.current.as_constructor_args(
|
82
82
|
page_class,
|
83
83
|
request_params: params,
|
84
84
|
route: brut_route,
|
85
85
|
)
|
86
86
|
span.add_prefixed_attributes("brut.initializer.args", constructor_args.map { |k,v| [k.to_s,v.class.name] }.to_h)
|
87
87
|
page_instance = page_class.new(**constructor_args)
|
88
|
+
|
88
89
|
result = page_instance.handle!
|
90
|
+
|
89
91
|
span.add_prefixed_attributes("brut", result_class: result.class)
|
90
92
|
case result
|
91
93
|
in URI => uri
|
@@ -177,7 +179,6 @@ module Brut::SinatraHelpers
|
|
177
179
|
form_class: form_class,
|
178
180
|
)
|
179
181
|
|
180
|
-
request_context = Thread.current.thread_variable_get(:request_context)
|
181
182
|
handler = handler_class.new
|
182
183
|
form = if form_class.nil?
|
183
184
|
nil
|
@@ -185,7 +186,7 @@ module Brut::SinatraHelpers
|
|
185
186
|
form_class.new(params: params)
|
186
187
|
end
|
187
188
|
|
188
|
-
process_args =
|
189
|
+
process_args = Brut::FrontEnd::RequestContext.current.as_method_args(handler,:handle,request_params: params,form: form,route:brut_route)
|
189
190
|
|
190
191
|
result = handler.handle!(**process_args)
|
191
192
|
|
@@ -193,12 +194,17 @@ module Brut::SinatraHelpers
|
|
193
194
|
in URI => uri
|
194
195
|
redirect to(uri.to_s)
|
195
196
|
in Brut::FrontEnd::Component => component_instance
|
196
|
-
|
197
|
-
in [
|
197
|
+
component_instance.call.to_s
|
198
|
+
in [
|
199
|
+
Brut::FrontEnd::Component => component_instance,
|
200
|
+
Brut::FrontEnd::HttpStatus => http_status,
|
201
|
+
]
|
202
|
+
|
198
203
|
[
|
199
204
|
http_status.to_i,
|
200
|
-
|
205
|
+
component_instance.call.to_s,
|
201
206
|
]
|
207
|
+
|
202
208
|
in Brut::FrontEnd::HttpStatus => http_status
|
203
209
|
http_status.to_i
|
204
210
|
in Brut::FrontEnd::Download => download
|
@@ -8,11 +8,19 @@ module Brut::SpecSupport::ComponentSupport
|
|
8
8
|
include Brut::SpecSupport::FlashSupport
|
9
9
|
include Brut::SpecSupport::SessionSupport
|
10
10
|
include Brut::SpecSupport::ClockSupport
|
11
|
-
include Brut::I18n::
|
11
|
+
include Brut::I18n::BaseMethods
|
12
12
|
|
13
|
-
# Render a component into its text representation. This mimics what happens when
|
14
|
-
#
|
15
|
-
#
|
13
|
+
# Render a component or page into its text representation. This mimics what happens when Brut renders
|
14
|
+
# the page or component. Note that pages don't always return Strings, for example if `before_render`
|
15
|
+
# returns a redirect.
|
16
|
+
#
|
17
|
+
# When testing a component, call {#render_and_parse} instead of this. When testing a page that will
|
18
|
+
# always render HTML, again call {#render_and_parse}.
|
19
|
+
#
|
20
|
+
# When using this, there are some matchers that can help assert what the page has done:
|
21
|
+
#
|
22
|
+
# * `have_redirected_to` to check that the page redirected elsewhere, instead of rendering.
|
23
|
+
# * `have_returned_http_status` to check that the page returned an HTTP status instead of rendering.
|
16
24
|
def render(component,&block)
|
17
25
|
if component.kind_of?(Brut::FrontEnd::Page)
|
18
26
|
if !block.nil?
|
@@ -20,12 +28,23 @@ module Brut::SpecSupport::ComponentSupport
|
|
20
28
|
end
|
21
29
|
component.handle!
|
22
30
|
else
|
23
|
-
|
24
|
-
|
31
|
+
if block.nil?
|
32
|
+
component.call
|
33
|
+
else
|
34
|
+
component.call do
|
35
|
+
component.raw(component.safe(block.()))
|
36
|
+
end
|
37
|
+
end
|
25
38
|
end
|
26
39
|
end
|
27
40
|
|
28
|
-
# Render a component and parse it into a Nokogiri Node for examination.
|
41
|
+
# Render a component or page and parse it into a Nokogiri Node for examination. There are several matchers
|
42
|
+
# you can use with the return value of this method:
|
43
|
+
#
|
44
|
+
# * `have_html_attribute` to check if a node has a value for an HTML attribute.
|
45
|
+
# * `have_i18n_string` to check if the text of a node is exactly an i18n string you have set up.
|
46
|
+
# * `have_link_to` to check that a node contains a link to a page or page routing
|
47
|
+
#
|
29
48
|
#
|
30
49
|
# @example
|
31
50
|
#
|
@@ -41,7 +60,7 @@ module Brut::SpecSupport::ComponentSupport
|
|
41
60
|
# @return [Brut::SpecSupport::EnhancedNode] a wrapper around a Nokogiri node to provide convienience methods.
|
42
61
|
def render_and_parse(component,&block)
|
43
62
|
rendered_text = render(component,&block)
|
44
|
-
if !rendered_text.kind_of?(String)
|
63
|
+
if !rendered_text.kind_of?(String)
|
45
64
|
if rendered_text.kind_of?(URI::Generic)
|
46
65
|
raise "#{component.class} redirected to #{rendered_text} instead of rendering"
|
47
66
|
else
|
@@ -80,9 +99,4 @@ module Brut::SpecSupport::ComponentSupport
|
|
80
99
|
def routing_for(klass,**args)
|
81
100
|
Brut.container.routing.path(klass,**args)
|
82
101
|
end
|
83
|
-
|
84
|
-
# Escape HTML using the same code Brut uses for rendering templates.
|
85
|
-
def escape_html(...)
|
86
|
-
Brut::FrontEnd::Templates::EscapableFilter.escape_html(...)
|
87
|
-
end
|
88
102
|
end
|
@@ -5,6 +5,9 @@ module Brut::SpecSupport::GeneralSupport
|
|
5
5
|
end
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
+
# Used to indicate that a test does need to be written, but that
|
9
|
+
# its implementation can wait until a given date before causing
|
10
|
+
# `bin/ci` to fail the test suite.
|
8
11
|
def implementation_is_needed(check_again_at:)
|
9
12
|
check_again_at = if check_again_at.kind_of?(Time)
|
10
13
|
check_again_at
|
@@ -2,7 +2,12 @@ require_relative "flash_support"
|
|
2
2
|
require_relative "clock_support"
|
3
3
|
require_relative "session_support"
|
4
4
|
|
5
|
-
# Convienience methods for testing handlers.
|
5
|
+
# Convienience methods for testing handlers. When testing handlers, the following matchers may be useful:
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# * `have_redirected_to` to check that the handler redirected to a give URI
|
9
|
+
# * `have_rendered` to check that the handler rendered a specific page
|
10
|
+
# * `have_returned_http_status` to check that the handler returned an HTTP status
|
6
11
|
module Brut::SpecSupport::HandlerSupport
|
7
12
|
include Brut::SpecSupport::FlashSupport
|
8
13
|
include Brut::SpecSupport::ClockSupport
|
@@ -10,4 +10,5 @@ require_relative "matchers/have_i18n_string"
|
|
10
10
|
require_relative "matchers/have_redirected_to"
|
11
11
|
require_relative "matchers/have_rendered"
|
12
12
|
require_relative "matchers/have_returned_http_status"
|
13
|
+
require_relative "matchers/have_returned_rack_response"
|
13
14
|
require_relative "matchers/have_link_to"
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Handler
|
2
|
+
RSpec::Matchers.define :have_returned_rack_response do |http_status: :any, headers: :any, body: :any|
|
3
|
+
match do |result|
|
4
|
+
case result
|
5
|
+
in [ response_status, response_headers, response_body ]
|
6
|
+
http_status_match = http_status == :any || response_status == http_status
|
7
|
+
headers_match = headers == :any || response_headers == headers
|
8
|
+
body_match = body == :any || response_body == body
|
9
|
+
|
10
|
+
http_status_match && headers_match && body_match
|
11
|
+
else
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
failure_message do |result|
|
17
|
+
case result
|
18
|
+
in [ response_status, response_headers, response_body ]
|
19
|
+
http_status_match = http_status == :any || response_status == http_status
|
20
|
+
headers_match = headers == :any || response_headers == headers
|
21
|
+
body_match = body == :any || response_body == body
|
22
|
+
errors = [
|
23
|
+
http_status_match ? nil : "HTTP status #{response_status} did not match #{http_status}",
|
24
|
+
headers_match ? nil : "Headers #{response_headers} did not match #{headers}",
|
25
|
+
body_match ? nil : "Body #{response_body} did not match #{body}",
|
26
|
+
].compact.join(", ")
|
27
|
+
else
|
28
|
+
if result.kind_of?(Array)
|
29
|
+
"Response was a #{result.class} of length #{result.length}, which could not be interpreted as a Rack response."
|
30
|
+
else
|
31
|
+
"Response was a #{result.class}, which could not be interpreted as a Rack response."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
failure_message_when_negated do |result|
|
36
|
+
case result
|
37
|
+
in [ response_status, response_headers, response_body ]
|
38
|
+
"Response was a Rack response and/or array of size 3"
|
39
|
+
else
|
40
|
+
"failure_message_when_negated encounterd a code-path for a non-Rack response, which should not have happened when have_returned_rack_response was negated"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -80,6 +80,7 @@ class Brut::SpecSupport::RSpecSetup
|
|
80
80
|
@config.include Brut::SpecSupport::GeneralSupport
|
81
81
|
@config.include Brut::SpecSupport::ComponentSupport, component: true
|
82
82
|
@config.include Brut::SpecSupport::HandlerSupport, handler: true
|
83
|
+
@config.include Brut::SpecSupport::E2eSupport, e2e: true
|
83
84
|
@config.include Playwright::Test::Matchers, e2e: true
|
84
85
|
|
85
86
|
@config.around do |example|
|
data/lib/brut/spec_support.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
module Brut
|
2
2
|
# Spec Support holds various matchers and helpers useful when writing tests with RSpec.
|
3
3
|
# Note that this module and it's contents aren't loaded by default when you `require "brut"`.
|
4
|
-
# Your app's `spec_helper.rb` should require these
|
4
|
+
# Your app's `spec_helper.rb` should require these explicitly.
|
5
5
|
module SpecSupport
|
6
6
|
end
|
7
7
|
end
|
8
|
-
require_relative "spec_support/matcher"
|
9
8
|
require_relative "spec_support/component_support"
|
10
|
-
require_relative "spec_support/
|
11
|
-
require_relative "spec_support/general_support"
|
9
|
+
require_relative "spec_support/e2e_support"
|
12
10
|
require_relative "spec_support/e2e_test_server"
|
11
|
+
require_relative "spec_support/general_support"
|
12
|
+
require_relative "spec_support/handler_support"
|
13
|
+
require_relative "spec_support/matcher"
|
13
14
|
require_relative "spec_support/rspec_setup"
|
14
15
|
require_relative "factory_bot"
|
15
16
|
# Convention here is different. We don't want to autoload
|
data/lib/brut/version.rb
CHANGED