brut 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/Gemfile.lock +66 -1
- data/README.md +36 -0
- data/Rakefile +22 -0
- data/brut.gemspec +7 -0
- data/doc-src/architecture.md +102 -0
- data/doc-src/assets.md +98 -0
- data/doc-src/forms.md +214 -0
- data/doc-src/handlers.md +83 -0
- data/doc-src/javascript.md +265 -0
- data/doc-src/keyword-injection.md +183 -0
- data/doc-src/pages.md +210 -0
- data/doc-src/route-hooks.md +59 -0
- data/docs-todo.md +32 -0
- data/lib/brut/back_end/seed_data.rb +5 -1
- data/lib/brut/back_end/validator.rb +1 -1
- data/lib/brut/back_end/validators/form_validator.rb +31 -6
- data/lib/brut/cli/app.rb +100 -4
- data/lib/brut/cli/app_runner.rb +38 -5
- data/lib/brut/cli/apps/build_assets.rb +4 -6
- data/lib/brut/cli/apps/db.rb +2 -7
- data/lib/brut/cli/apps/scaffold.rb +413 -7
- data/lib/brut/cli/apps/test.rb +14 -1
- data/lib/brut/cli/command.rb +141 -6
- data/lib/brut/cli/error.rb +30 -3
- data/lib/brut/cli/execution_results.rb +44 -6
- data/lib/brut/cli/executor.rb +21 -1
- data/lib/brut/cli/options.rb +29 -2
- data/lib/brut/cli/output.rb +47 -0
- data/lib/brut/cli.rb +26 -0
- data/lib/brut/factory_bot.rb +2 -0
- data/lib/brut/framework/app.rb +38 -5
- data/lib/brut/framework/config.rb +97 -54
- data/lib/brut/framework/container.rb +97 -33
- data/lib/brut/framework/errors/abstract_method.rb +3 -7
- data/lib/brut/framework/errors/missing_parameter.rb +12 -0
- data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
- data/lib/brut/framework/errors/not_found.rb +12 -2
- data/lib/brut/framework/errors/not_implemented.rb +14 -0
- data/lib/brut/framework/errors.rb +18 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
- data/lib/brut/framework/mcp.rb +106 -49
- data/lib/brut/framework/project_environment.rb +9 -0
- data/lib/brut/framework.rb +1 -0
- data/lib/brut/front_end/asset_metadata.rb +7 -1
- data/lib/brut/front_end/component.rb +129 -38
- data/lib/brut/front_end/components/constraint_violations.rb +57 -0
- data/lib/brut/front_end/components/form_tag.rb +23 -32
- data/lib/brut/front_end/components/i18n_translations.rb +34 -1
- data/lib/brut/front_end/components/input.rb +3 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
- data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
- data/lib/brut/front_end/components/inputs/select.rb +26 -2
- data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
- data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
- data/lib/brut/front_end/components/locale_detection.rb +8 -1
- data/lib/brut/front_end/components/page_identifier.rb +2 -0
- data/lib/brut/front_end/components/time.rb +95 -0
- data/lib/brut/front_end/components/traceparent.rb +22 -0
- data/lib/brut/front_end/download.rb +11 -0
- data/lib/brut/front_end/flash.rb +32 -0
- data/lib/brut/front_end/form.rb +109 -106
- data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
- data/lib/brut/front_end/forms/input.rb +30 -42
- data/lib/brut/front_end/forms/input_declarations.rb +90 -0
- data/lib/brut/front_end/forms/input_definition.rb +45 -30
- data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
- data/lib/brut/front_end/forms/select_input.rb +47 -0
- data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
- data/lib/brut/front_end/forms/validity_state.rb +23 -9
- data/lib/brut/front_end/handler.rb +27 -8
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
- data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
- data/lib/brut/front_end/handling_results.rb +14 -4
- data/lib/brut/front_end/http_method.rb +13 -1
- data/lib/brut/front_end/http_status.rb +10 -0
- data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
- data/lib/brut/front_end/middleware.rb +5 -0
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
- data/lib/brut/front_end/middlewares/favicon.rb +16 -0
- data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
- data/lib/brut/front_end/page.rb +50 -11
- data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
- data/lib/brut/front_end/pages/missing_page.rb +36 -0
- data/lib/brut/front_end/request_context.rb +117 -8
- data/lib/brut/front_end/route_hook.rb +43 -1
- data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
- data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
- data/lib/brut/front_end/routing.rb +138 -31
- data/lib/brut/front_end/session.rb +86 -7
- data/lib/brut/front_end/template.rb +17 -2
- data/lib/brut/front_end/templates/block_filter.rb +4 -3
- data/lib/brut/front_end/templates/erb_parser.rb +1 -1
- data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
- data/lib/brut/front_end/templates/locator.rb +60 -0
- data/lib/brut/i18n/base_methods.rb +4 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
- data/lib/brut/instrumentation/open_telemetry.rb +107 -0
- data/lib/brut/instrumentation.rb +4 -6
- data/lib/brut/junk_drawer.rb +54 -4
- data/lib/brut/sinatra_helpers.rb +42 -38
- data/lib/brut/spec_support/clock_support.rb +6 -0
- data/lib/brut/spec_support/component_support.rb +53 -26
- data/lib/brut/spec_support/e2e_test_server.rb +82 -0
- data/lib/brut/spec_support/enhanced_node.rb +45 -0
- data/lib/brut/spec_support/general_support.rb +14 -3
- data/lib/brut/spec_support/handler_support.rb +2 -0
- data/lib/brut/spec_support/matcher.rb +6 -3
- data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
- data/lib/brut/spec_support/rspec_setup.rb +182 -0
- data/lib/brut/spec_support.rb +8 -3
- data/lib/brut/version.rb +2 -1
- data/lib/brut.rb +28 -5
- data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
- data/lib/sequel/extensions/brut_migrations.rb +18 -8
- data/lib/sequel/plugins/created_at.rb +2 -0
- data/lib/sequel/plugins/external_id.rb +39 -1
- data/lib/sequel/plugins/find_bang.rb +4 -1
- metadata +140 -13
- data/lib/brut/back_end/action.rb +0 -3
- data/lib/brut/back_end/result.rb +0 -46
- data/lib/brut/front_end/components/timestamp.rb +0 -33
- data/lib/brut/instrumentation/basic.rb +0 -66
- data/lib/brut/instrumentation/event.rb +0 -19
- data/lib/brut/instrumentation/http_event.rb +0 -5
- data/lib/brut/instrumentation/subscriber.rb +0 -41
@@ -1,7 +1,9 @@
|
|
1
1
|
require_relative "flash_support"
|
2
|
+
require_relative "clock_support"
|
2
3
|
require_relative "session_support"
|
3
4
|
module Brut::SpecSupport::HandlerSupport
|
4
5
|
include Brut::SpecSupport::FlashSupport
|
6
|
+
include Brut::SpecSupport::ClockSupport
|
5
7
|
include Brut::SpecSupport::SessionSupport
|
6
8
|
end
|
7
9
|
|
@@ -1,9 +1,12 @@
|
|
1
1
|
module Brut::SpecSupport::Matchers
|
2
2
|
end
|
3
|
+
require_relative "matchers/be_a_bug"
|
4
|
+
require_relative "matchers/be_page_for"
|
5
|
+
require_relative "matchers/be_routing_for"
|
3
6
|
require_relative "matchers/have_constraint_violation"
|
4
7
|
require_relative "matchers/have_html_attribute"
|
5
|
-
require_relative "matchers/
|
6
|
-
require_relative "matchers/
|
8
|
+
require_relative "matchers/have_i18n_string"
|
9
|
+
require_relative "matchers/have_redirected_to"
|
7
10
|
require_relative "matchers/have_rendered"
|
8
11
|
require_relative "matchers/have_returned_http_status"
|
9
|
-
require_relative "matchers/
|
12
|
+
require_relative "matchers/have_link_to"
|
@@ -6,9 +6,10 @@ RSpec::Matchers.define :be_page_for do |klass|
|
|
6
6
|
failure_message do |page|
|
7
7
|
meta = page.locator("meta[name='class']")
|
8
8
|
if meta.count == 0
|
9
|
-
"Could not find <meta name='class'> on the page:\n\n#{page.content}"
|
9
|
+
"Could not find <meta name='class'> on the page, which is what's needed to know what page we are on:\n\n#{page.content}"
|
10
10
|
else
|
11
|
-
|
11
|
+
page_name = meta.get_attribute("content")
|
12
|
+
"Expected to be on page #{klass.name}, but we seem to be on page #{page_name}, based on this meta tag:\n\n#{meta.evaluate('e => e.outerHTML')}"
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -1,14 +1,26 @@
|
|
1
|
-
RSpec::Matchers.define :have_constraint_violation do |field,key
|
1
|
+
RSpec::Matchers.define :have_constraint_violation do |field,key:,index:nil|
|
2
2
|
match do |form|
|
3
|
-
Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key).matches?
|
3
|
+
Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key,index).matches?
|
4
4
|
end
|
5
5
|
|
6
6
|
failure_message do |form|
|
7
|
-
analysis = Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key)
|
7
|
+
analysis = Brut::SpecSupport::Matchers::HaveConstraintViolation.new(form,field,key,index)
|
8
8
|
if analysis.found_field?
|
9
|
-
"#{field} did not have #{key} as a violation. These keys were found: #{analysis.keys_on_field_found.map(&:to_s).join(", ")}"
|
9
|
+
"Field '#{field}' did not have key '#{key}' as a violation. These keys were found: #{analysis.keys_on_field_found.map(&:to_s).join(", ")}"
|
10
10
|
else
|
11
|
-
|
11
|
+
field_searched_for = if index.nil?
|
12
|
+
field
|
13
|
+
else
|
14
|
+
"#{field}, index #{index}"
|
15
|
+
end
|
16
|
+
fields_with_errors = analysis.fields_found.map { |(field,index)|
|
17
|
+
if index.nil?
|
18
|
+
field
|
19
|
+
else
|
20
|
+
"#{field}, index #{index}"
|
21
|
+
end
|
22
|
+
}.join(", ")
|
23
|
+
"Field '#{field_searched_for}' had no errors. These fields DID: #{fields_with_errors}"
|
12
24
|
end
|
13
25
|
end
|
14
26
|
|
@@ -21,21 +33,22 @@ class Brut::SpecSupport::Matchers::HaveConstraintViolation
|
|
21
33
|
attr_reader :fields_found
|
22
34
|
attr_reader :keys_on_field_found
|
23
35
|
|
24
|
-
def initialize(form, field, key)
|
36
|
+
def initialize(form, field, key, index)
|
25
37
|
if !form.kind_of?(Brut::FrontEnd::Form)
|
26
38
|
raise "#{self.class} only works with forms, not #{form.class}"
|
27
39
|
end
|
28
40
|
@form = form
|
29
41
|
@field = field.to_s
|
30
42
|
@key = key.to_s
|
43
|
+
@index = index || 0
|
31
44
|
|
32
45
|
@matches = false
|
33
46
|
@found_field = false
|
34
47
|
@fields_found = Set.new
|
35
48
|
@keys_on_field_found = Set.new
|
36
49
|
|
37
|
-
@form.constraint_violations.each do |input_name, constraint_violations|
|
38
|
-
if input_name.to_s == @field
|
50
|
+
@form.constraint_violations.each do |input_name, (constraint_violations, index)|
|
51
|
+
if input_name.to_s == @field && index == @index
|
39
52
|
@found_field = true
|
40
53
|
constraint_violations.each do |constraint_violation|
|
41
54
|
if constraint_violation.key.to_s == @key
|
@@ -45,7 +58,7 @@ class Brut::SpecSupport::Matchers::HaveConstraintViolation
|
|
45
58
|
end
|
46
59
|
end
|
47
60
|
else
|
48
|
-
@fields_found << input_name.to_s
|
61
|
+
@fields_found << [ input_name.to_s, index ]
|
49
62
|
end
|
50
63
|
end
|
51
64
|
end
|
@@ -38,8 +38,15 @@ class Brut::SpecSupport::Matchers::HaveHTMLAttribute
|
|
38
38
|
else
|
39
39
|
result = result.first
|
40
40
|
end
|
41
|
-
|
42
|
-
|
41
|
+
else
|
42
|
+
object_to_check = if result.kind_of?(SimpleDelegator)
|
43
|
+
result.__getobj__
|
44
|
+
else
|
45
|
+
result
|
46
|
+
end
|
47
|
+
if !object_to_check.kind_of?(Nokogiri::XML::Element)
|
48
|
+
@error = "Received a #{result.class} instead of a NodeSet or Element, as could be returned by `.css(...)`"
|
49
|
+
end
|
43
50
|
end
|
44
51
|
if !@error
|
45
52
|
if attribute.kind_of?(Hash)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
RSpec::Matchers.define :have_i18n_string do |key,**args|
|
2
|
+
include Brut::I18n::ForHTML
|
3
|
+
match do |nokogiri_node|
|
4
|
+
|
5
|
+
text = nokogiri_node.text.strip
|
6
|
+
i18n_string = t(key,**args).to_s
|
7
|
+
|
8
|
+
text == i18n_string
|
9
|
+
end
|
10
|
+
|
11
|
+
failure_message do |nokogiri_node|
|
12
|
+
text = nokogiri_node.text.strip
|
13
|
+
begin
|
14
|
+
"Expected '#{text}' to be '#{t(key,**args)}'\n#{nokogiri_node.to_html}\n"
|
15
|
+
rescue => ex
|
16
|
+
"I18n key '#{key}' could not be found: #{ex.message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
failure_message_when_negated do |nokogiri_node|
|
21
|
+
"Did not expect node's text to be '#{t(key,**args)}'\n#{nokogiri_node.to_html}\n"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
RSpec::Matchers.define :have_link_to do |page_klass,**args|
|
2
|
+
match do |node|
|
3
|
+
node.css("a[href='#{page_klass.routing(**args)}']").any?
|
4
|
+
end
|
5
|
+
|
6
|
+
failure_message do |node|
|
7
|
+
links = node.css("a").map(&:to_html)
|
8
|
+
"Did not find link to #{page_klass.routing(**args)}. Found these links: #{links.join(',')}"
|
9
|
+
end
|
10
|
+
|
11
|
+
failure_message_when_negated do |result|
|
12
|
+
"Did not expect to find link to #{page_klass.routing(**args)}."
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
RSpec::Matchers.define :have_redirected_to do |page_or_uri,**page_params|
|
2
|
+
match do |result|
|
3
|
+
if page_or_uri.kind_of?(URI)
|
4
|
+
if !page_params.empty?
|
5
|
+
raise "have_redirected_to, when given a URI, must NOT be given parameters. Got '#{page_params}'"
|
6
|
+
end
|
7
|
+
result == page_or_uri
|
8
|
+
elsif page_or_uri.ancestors.include?(Brut::FrontEnd::Page)
|
9
|
+
result == page_or_uri.routing(**page_params)
|
10
|
+
else
|
11
|
+
raise "have_redirected_to must be given a URI or a Brut::FrontEnd::Page class, got #{page_or_uri.class}"
|
12
|
+
end
|
13
|
+
rescue Brut::Framework::Errors::MissingParameter => ex
|
14
|
+
raise "#{page_or_uri}'s routing requires parameters you must specicfy to `have_redirected_to`: #{ex.message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
failure_message do |result|
|
18
|
+
if page_or_uri.kind_of?(URI)
|
19
|
+
"Expected #{page_or_uri} but got #{result}"
|
20
|
+
elsif page_or_uri.ancestors.include?(Brut::FrontEnd::Page)
|
21
|
+
"Expected #{page_or_uri}'s routing (#{page_or_uri.routing(**page_params)}), but got #{result}"
|
22
|
+
else
|
23
|
+
"Unknown error occured or bug with have_redirected_to"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
failure_message_when_negated do |result|
|
27
|
+
"Got a redirect when it wasn't expected"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -1,20 +1,13 @@
|
|
1
|
-
RSpec::Matchers.define :have_rendered do
|
1
|
+
RSpec::Matchers.define :have_rendered do |component_or_page|
|
2
2
|
match do |result|
|
3
|
-
result.
|
3
|
+
result.class.ancestors.include?(component_or_page)
|
4
4
|
end
|
5
5
|
|
6
6
|
failure_message do |result|
|
7
|
-
|
8
|
-
in URI => uri
|
9
|
-
"Got a redirect to #{uri} instead of rendering"
|
10
|
-
in Brut::FrontEnd::HttpStatus => http_status
|
11
|
-
"Got an HTTP status of #{http_status} instead of rendering"
|
12
|
-
else
|
13
|
-
"Got an unexpected result: #{result.class} instead of a String"
|
14
|
-
end
|
7
|
+
"Expected a #{component_or_page} to be rendered, but got #{result}"
|
15
8
|
end
|
16
9
|
failure_message_when_negated do |result|
|
17
|
-
"
|
10
|
+
"Got #{component_or_page} when not expected"
|
18
11
|
end
|
19
12
|
|
20
13
|
end
|
@@ -1,12 +1,16 @@
|
|
1
|
-
RSpec::Matchers.define :have_returned_http_status do |http_status|
|
1
|
+
RSpec::Matchers.define :have_returned_http_status do |http_status=nil|
|
2
2
|
match do |result|
|
3
|
-
|
4
|
-
|
5
|
-
http_status == 302
|
6
|
-
in Brut::FrontEnd::HttpStatus => result_http_status
|
7
|
-
http_status == result_http_status.to_i
|
3
|
+
if http_status.nil?
|
4
|
+
result.kind_of?(Brut::FrontEnd::HttpStatus)
|
8
5
|
else
|
9
|
-
|
6
|
+
case result
|
7
|
+
in URI => uri
|
8
|
+
http_status == 302
|
9
|
+
in Brut::FrontEnd::HttpStatus => result_http_status
|
10
|
+
http_status == result_http_status.to_i
|
11
|
+
else
|
12
|
+
http_status == 200
|
13
|
+
end
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
@@ -21,7 +25,11 @@ RSpec::Matchers.define :have_returned_http_status do |http_status|
|
|
21
25
|
end
|
22
26
|
end
|
23
27
|
failure_message_when_negated do |result|
|
24
|
-
|
28
|
+
if http_status.nil?
|
29
|
+
"#{result} was rendered, but was not expecting an HTTP status"
|
30
|
+
else
|
31
|
+
"Got #{http_status} when not expected (#{result.class} was returned)"
|
32
|
+
end
|
25
33
|
end
|
26
34
|
|
27
35
|
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# Configures RSpec for Brut. This provides several bits of infrastructure only present when the app is running, as well as
|
2
|
+
# quality of life improvements to make testing Brut components a bit easier. Even though Brut classes are all normal classes, it's
|
3
|
+
# convenient to have extraneous classes set up for you.
|
4
|
+
#
|
5
|
+
# * Metadata is added based on the class names under test:
|
6
|
+
# - `*Component` -> `:component`
|
7
|
+
# - `*Page` -> `:component`
|
8
|
+
# - `*Page` -> `:page`
|
9
|
+
# - `*Handler` -> `:handler`
|
10
|
+
# - If you add the `:page` metadata automatically, `:component` is added as well.
|
11
|
+
# - Tests in `specs/e2e` or any subfolder -> `:e2e`
|
12
|
+
# - Note that you are free to explicitly add these tags to your test metadata. That will cause the inclusion of modules as described below
|
13
|
+
# * Modules are included to provide additional support for writing tests:
|
14
|
+
# - {Brut::SpecSupport::GeneralSupport} included in all tests
|
15
|
+
# - {Brut::SpecSupport::ComponentSupport} included when `:component` metadata is set (generally, this is for page and component tests)
|
16
|
+
# - {Brut::SpecSupport::HandlerSupport} included when `:handler` is set.
|
17
|
+
# - {Playwright::Test::Matchers{ included when `:e2e` is set, which allows use of the Ruby binding for Playwright.
|
18
|
+
# * Non end-to-end tests will be run inside a database transaction to allow all database changes to be instantly undone. This does
|
19
|
+
# mean that any tests that tests database transaction behavior will not work as expected.
|
20
|
+
# * In component tests (which generally includes page tests), a {Brut::FrontEnd::RequestContext} is created for you and placed into
|
21
|
+
# the thread local storage. This allows any component that is injected with data from the `RequestContext` to access it as it would
|
22
|
+
# normally. You can also seed this with data that a component may need.
|
23
|
+
# * Handles all infrastructure for end-to-end tests:
|
24
|
+
# - starting a test server using {Brut::SpecSupport::E2ETestServer}
|
25
|
+
# - launching Chromium via Playwright
|
26
|
+
# * If using Sidekiq:
|
27
|
+
# - Jobs are cleared before each test
|
28
|
+
# - For end-to-end tests, Redis is flushed and actual Sidekiq is used instead of testing mode
|
29
|
+
#
|
30
|
+
# You can set certain metadata to change behavior:
|
31
|
+
#
|
32
|
+
# * `e2e_timeout` - number of milliseconds to wait until an end-to-end test gives up on a selector being found. The default is 5,000 (5 seconds). Use this only if there's no other way to keep your test from needing more than 5 seconds.
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# RSpec.configure do |config|
|
37
|
+
# rspec_setup = Brut::SpecSupport::RSpecSetup.new(rspec_config: config)
|
38
|
+
# rspec_setup.setup!
|
39
|
+
#
|
40
|
+
# # rest of the RSpec configuration
|
41
|
+
# end
|
42
|
+
class Brut::SpecSupport::RSpecSetup
|
43
|
+
# Create the setup with the given RSpec configuration.
|
44
|
+
#
|
45
|
+
# @param [RSpec::Core::Configuration] rspec_config yielded from `RSpec.configure`
|
46
|
+
def initialize(rspec_config:)
|
47
|
+
@config = rspec_config
|
48
|
+
SemanticLogger.default_level = ENV.fetch("LOGGER_LEVEL_FOR_TESTS","warn")
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets up RSpec with variouis configurations needed by Brut to run your tests.
|
52
|
+
#
|
53
|
+
# @param [Proc] inside_db_transaction if given, this is run inside the DB transaction before your example is run. This is useful if you need to set up some reference data for all tests.
|
54
|
+
def setup!(inside_db_transaction: ->() {})
|
55
|
+
|
56
|
+
Brut::FactoryBot.new.setup!
|
57
|
+
optional_sidekiq_support = OptionalSidekiqSupport.new
|
58
|
+
|
59
|
+
@config.define_derived_metadata do |metadata|
|
60
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Component$/ ||
|
61
|
+
metadata[:described_class].to_s =~ /[a-z0-9]Page$/ ||
|
62
|
+
metadata[:page] == true
|
63
|
+
metadata[:component] = true
|
64
|
+
end
|
65
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Page$/ ||
|
66
|
+
metadata[:page] == true
|
67
|
+
metadata[:page] = true
|
68
|
+
end
|
69
|
+
if metadata[:described_class].to_s =~ /[a-z0-9]Handler$/
|
70
|
+
metadata[:handler] = true
|
71
|
+
end
|
72
|
+
|
73
|
+
relative_path = Pathname(metadata[:absolute_file_path]).relative_path_from(Brut.container.app_specs_dir)
|
74
|
+
|
75
|
+
top_level_directory = relative_path.each_filename.to_a[0].to_s
|
76
|
+
if top_level_directory == "e2e"
|
77
|
+
metadata[:e2e] = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@config.include Brut::SpecSupport::GeneralSupport
|
81
|
+
@config.include Brut::SpecSupport::ComponentSupport, component: true
|
82
|
+
@config.include Brut::SpecSupport::HandlerSupport, handler: true
|
83
|
+
@config.include Playwright::Test::Matchers, e2e: true
|
84
|
+
|
85
|
+
@config.around do |example|
|
86
|
+
|
87
|
+
needs_request_context = example.metadata[:component] ||
|
88
|
+
example.metadata[:handler] ||
|
89
|
+
example.metadata[:page]
|
90
|
+
|
91
|
+
if needs_request_context
|
92
|
+
session = {
|
93
|
+
"session_id" => "test-session-id",
|
94
|
+
"csrf" => "test-csrf-token"
|
95
|
+
}
|
96
|
+
env = {
|
97
|
+
"rack.session" => session
|
98
|
+
}
|
99
|
+
app_session = Brut.container.session_class.new(rack_session: session)
|
100
|
+
request_context = Brut::FrontEnd::RequestContext.new(
|
101
|
+
env: env,
|
102
|
+
session: app_session,
|
103
|
+
flash: empty_flash,
|
104
|
+
body: nil,
|
105
|
+
xhr: false,
|
106
|
+
)
|
107
|
+
Thread.current.thread_variable_set(:request_context, request_context)
|
108
|
+
example.example_group.let(:request_context) { request_context }
|
109
|
+
end
|
110
|
+
if example.metadata[:component]
|
111
|
+
example.example_group.let(:component_name) { described_class.component_name }
|
112
|
+
end
|
113
|
+
if example.metadata[:page]
|
114
|
+
example.example_group.let(:page_name) { described_class.page_name }
|
115
|
+
end
|
116
|
+
|
117
|
+
if example.metadata[:e2e]
|
118
|
+
e2e_timeout = (ENV["E2E_TIMEOUT_MS"] || example.metadata[:e2e_timeout] || 5_000).to_i
|
119
|
+
optional_sidekiq_support.disable_sidekiq_testing do
|
120
|
+
Brut::SpecSupport::E2ETestServer.instance.start
|
121
|
+
Playwright.create(playwright_cli_executable_path: "./node_modules/.bin/playwright") do |playwright|
|
122
|
+
launch_args = {
|
123
|
+
headless: true,
|
124
|
+
}
|
125
|
+
if ENV["E2E_SLOW_MO"]
|
126
|
+
launch_args[:slowMo] = ENV["E2E_SLOW_MO"].to_i
|
127
|
+
end
|
128
|
+
playwright.chromium.launch(**launch_args) do |browser|
|
129
|
+
context_options = {
|
130
|
+
baseURL: "http://0.0.0.0:6503/",
|
131
|
+
}
|
132
|
+
if ENV["E2E_RECORD_VIDEOS"]
|
133
|
+
context_options[:record_video_dir] = Brut.container.tmp_dir / "e2e-videos"
|
134
|
+
end
|
135
|
+
browser_context = browser.new_context(**context_options)
|
136
|
+
browser_context.default_timeout = e2e_timeout
|
137
|
+
example.example_group.let(:page) { browser_context.new_page }
|
138
|
+
example.run
|
139
|
+
browser_context.close
|
140
|
+
browser.close
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
else
|
145
|
+
optional_sidekiq_support.clear_background_jobs
|
146
|
+
Sequel::Model.db.transaction do
|
147
|
+
inside_db_transaction.()
|
148
|
+
example.run
|
149
|
+
raise Sequel::Rollback
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
@config.after(:suite) do
|
154
|
+
Brut::SpecSupport::E2ETestServer.instance.stop
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
class OptionalSidekiqSupport
|
159
|
+
def initialize
|
160
|
+
@sidekiq_in_use = defined?(Sidekiq)
|
161
|
+
end
|
162
|
+
|
163
|
+
def disable_sidekiq_testing(&block)
|
164
|
+
if @sidekiq_in_use
|
165
|
+
Sidekiq::Testing.disable! do
|
166
|
+
Sidekiq.redis do |redis|
|
167
|
+
redis.flushall
|
168
|
+
end
|
169
|
+
block.()
|
170
|
+
end
|
171
|
+
else
|
172
|
+
block.()
|
173
|
+
end
|
174
|
+
end
|
175
|
+
def clear_background_jobs
|
176
|
+
if @sidekiq_in_use
|
177
|
+
Sidekiq::Worker.clear_all
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
end
|
data/lib/brut/spec_support.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
# Convention here is different. We don't want to autoload
|
2
|
-
# a lot of stuff, since RSpec pollutes the Object namespace.
|
3
|
-
# Instead, we'll require that these files are required explicitly
|
4
1
|
module Brut
|
2
|
+
# Spec Support holds various matchers and helpers useful when writing tests with RSpec.
|
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 properly.
|
5
5
|
module SpecSupport
|
6
6
|
end
|
7
7
|
end
|
@@ -9,4 +9,9 @@ require_relative "spec_support/matcher"
|
|
9
9
|
require_relative "spec_support/component_support"
|
10
10
|
require_relative "spec_support/handler_support"
|
11
11
|
require_relative "spec_support/general_support"
|
12
|
+
require_relative "spec_support/e2e_test_server"
|
13
|
+
require_relative "spec_support/rspec_setup"
|
12
14
|
require_relative "factory_bot"
|
15
|
+
# Convention here is different. We don't want to autoload
|
16
|
+
# a lot of stuff, since RSpec pollutes the Object namespace.
|
17
|
+
# Instead, we'll require that these files are required explicitly
|
data/lib/brut/version.rb
CHANGED
data/lib/brut.rb
CHANGED
@@ -1,10 +1,29 @@
|
|
1
1
|
require_relative "brut/framework"
|
2
2
|
|
3
|
-
#
|
3
|
+
# Brut is a way to make web apps with Ruby. It focuses on web standards, object-orientation, and other fundamentals. Brut seeks to
|
4
|
+
# minimize abstractions where possible.
|
4
5
|
#
|
5
|
-
#
|
6
|
-
#
|
6
|
+
# Brut encourages the use of the browser's technology and encourages you to build a web app based on good practices that are set up by
|
7
|
+
# default. Brut may not look easy, but it aims to be simple. It attempts to minimize dependencies and complexity, while leveraging
|
8
|
+
# common tested Ruby libraries related to web development.
|
9
|
+
#
|
10
|
+
# Have fun!
|
7
11
|
module Brut
|
12
|
+
# In Brut, the _front end_ is considered anything that interacts directly with a web browser or HTTP. This includes rendering HTML,
|
13
|
+
# managing JavaScript and CSS, and processing form submissions. It contrasts to {Brut::BackEnd}, which handles the business logic
|
14
|
+
# and database.
|
15
|
+
#
|
16
|
+
# You {Brut::App} defines pages, forms, and actions. A page is backed by a subclass of {Brut::FrontEnd::Page}, which provides
|
17
|
+
# dynamic data for rendering. A page can reference {Brut::FrontEnd::Component} subclasses to allow functional decomposition of front
|
18
|
+
# end logic and markup, as well as re-use. Both pages and components have ERB files that describe the HTML to be rendered.
|
19
|
+
#
|
20
|
+
# A {Brut::FrontEnd::Form} subclass defines a form that a browser will submit to your app. That
|
21
|
+
# submission is processed by a {Brut::FrontEnd::Handler} subclass. Handlers can also respond to other HTTP requests.
|
22
|
+
#
|
23
|
+
# In addition to responding to requests, you can subclass {Brut::FrontEnd::RouteHook} or {Brut::FrontEnd::Middleware} to perform
|
24
|
+
# further manipulation of the request.
|
25
|
+
#
|
26
|
+
# The entire front-end is based on Rack, so you should be able to achieve anything you need to.
|
8
27
|
module FrontEnd
|
9
28
|
autoload(:AssetMetadata, "brut/front_end/asset_metadata")
|
10
29
|
autoload(:Component, "brut/front_end/component")
|
@@ -20,19 +39,23 @@ module Brut
|
|
20
39
|
autoload(:Middleware, "brut/front_end/middleware")
|
21
40
|
autoload(:Middlewares, "brut/front_end/middleware")
|
22
41
|
autoload(:Page, "brut/front_end/page")
|
42
|
+
autoload(:Pages, "brut/front_end/page")
|
23
43
|
autoload(:RequestContext, "brut/front_end/request_context")
|
24
44
|
autoload(:RouteHook, "brut/front_end/route_hook")
|
25
45
|
autoload(:RouteHooks, "brut/front_end/route_hook")
|
26
46
|
autoload(:Routing, "brut/front_end/routing")
|
27
47
|
autoload(:Session, "brut/front_end/session")
|
28
48
|
end
|
49
|
+
# The _back end_ of a Brut app is where your app's business logic and database are managed. While the bulk of your Brut app's code
|
50
|
+
# will be in the back end, Brut is far less prescriptive about how to manage that than it is the front end.
|
29
51
|
module BackEnd
|
30
|
-
autoload(:Result, "brut/back_end/result")
|
31
52
|
autoload(:Validators, "brut/back_end/validator")
|
53
|
+
# Do not put SeedData here - it must be loaded only when needed
|
32
54
|
end
|
33
|
-
#
|
55
|
+
# I18n is where internationalization and localization support lives.
|
34
56
|
autoload(:I18n, "brut/i18n")
|
35
57
|
autoload(:Instrumentation,"brut/instrumentation")
|
36
58
|
autoload(:SinatraHelpers, "brut/sinatra_helpers")
|
59
|
+
# DO NOT autoload(:CLI) - that is intended to be require-able on its own
|
37
60
|
end
|
38
61
|
require "sequel/plugins"
|
@@ -1,37 +1,15 @@
|
|
1
1
|
module Sequel
|
2
2
|
module Extensions
|
3
|
+
# Instruments all SQL executions.
|
3
4
|
module BrutInstrumentation
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
end
|
8
|
-
end
|
9
|
-
def execute(sql, opts = Sequel::OPTS, &block)
|
10
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute", sql: sql)) do
|
11
|
-
super
|
12
|
-
end
|
13
|
-
end
|
14
|
-
def execute_dui(sql, opts = Sequel::OPTS, &block)
|
15
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute_dui", sql: sql)) do
|
16
|
-
super
|
17
|
-
end
|
18
|
-
end
|
19
|
-
def execute_insert(sql, opts = Sequel::OPTS, &block)
|
20
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "execute_insert", sql: sql)) do
|
21
|
-
super
|
22
|
-
end
|
23
|
-
end
|
24
|
-
def insert_select(*values)
|
25
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "insert_select", sql: values)) do
|
26
|
-
super
|
27
|
-
end
|
28
|
-
end
|
29
|
-
def returning_fetch_rows(sql,&block)
|
30
|
-
Brut.container.instrumentation.instrument(Event.new(operation: "returning_fetch_rows", sql: sql)) do
|
5
|
+
# @!visibility private
|
6
|
+
def log_connection_yield(sql,conn,args=nil)
|
7
|
+
Brut.container.instrumentation.span("SQL", sql: sql) do |span|
|
31
8
|
super
|
32
9
|
end
|
33
10
|
end
|
34
11
|
end
|
35
12
|
end
|
36
13
|
Sequel::Dataset.register_extension(:brut_instrumentation, Sequel::Extensions::BrutInstrumentation)
|
14
|
+
Sequel::Database.register_extension(:brut_instrumentation, Sequel::Extensions::BrutInstrumentation)
|
37
15
|
end
|
@@ -1,15 +1,20 @@
|
|
1
1
|
module Sequel
|
2
2
|
module Extensions
|
3
|
-
#
|
3
|
+
# Modifies and enhances Sequel's migrations DSL to default to best practices.
|
4
4
|
#
|
5
|
-
# * If no primary key is specified, a primary key column named
|
6
|
-
# * If no created_at is specified, a column name created_at of type timestamptz is created
|
7
|
-
# * create_table requires a comment
|
8
|
-
# * create_table accepts an external_id: true
|
9
|
-
# *
|
10
|
-
# *
|
11
|
-
# *
|
5
|
+
# * If no primary key is specified, a primary key column named `id` of type `int` will be created.
|
6
|
+
# * If no `created_at` is specified, a column name `created_at` of type `timestamptz` is created.
|
7
|
+
# * `create_table` requires a `comment:` attribute that explains the purpose of the table.
|
8
|
+
# * `create_table` accepts an `external_id: true` attribute that will create a unique `citext` field named `external_id`. This is intended to be used with {Sequel::Plugins::ExternalId}.
|
9
|
+
# * Columns are non-null by default. To make a nullable column, use `null: true`.
|
10
|
+
# * Foreign keys are non-null by default and an index is created by default.
|
11
|
+
# * The `key` method allows specifying additional keys on the table. This effecitvely creates a unique constraint on the fields given to `key`.
|
12
12
|
module BrutMigrations
|
13
|
+
# Overrides Sequel's `create_table`
|
14
|
+
#
|
15
|
+
# @param args [Object] the arguments to pass to Sequel's `create_table`. If the last entry in `*args` is a `Hash`, new options are recognized:
|
16
|
+
# @option args [String] :comment String containing the table's description, included in the table definition. Required.
|
17
|
+
# @option args [true|false] :external_id If true, adds a `:citext` column named `external_id` that has a unique index on it.
|
13
18
|
def create_table(*args)
|
14
19
|
super
|
15
20
|
|
@@ -27,10 +32,15 @@ module Sequel
|
|
27
32
|
end
|
28
33
|
end
|
29
34
|
|
35
|
+
# Specifies a non-primary key based on the fields given. Effectively creates a unique index on these fields.
|
36
|
+
# Inside a `create_table` block, this can be called via `key`
|
37
|
+
#
|
38
|
+
# @param fields [Array] fields that should form the key.
|
30
39
|
def add_key(fields)
|
31
40
|
add_index fields, unique: true
|
32
41
|
end
|
33
42
|
|
43
|
+
# Overrides Sequel's `add_column` to default `null: false`.
|
34
44
|
def add_column(table,*args)
|
35
45
|
options = args.last
|
36
46
|
if options.is_a?(Hash)
|