brut 0.0.26 → 0.0.27

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/brutrb.com/.vitepress/config.mjs +1 -0
  4. data/brutrb.com/ai.md +6 -1
  5. data/brutrb.com/components.md +1 -1
  6. data/brutrb.com/forms.md +22 -26
  7. data/brutrb.com/getting-started.md +56 -21
  8. data/brutrb.com/lsp.md +23 -0
  9. data/lib/brut/cli/apps/test.rb +2 -1
  10. data/lib/brut/front_end/component.rb +33 -9
  11. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +16 -74
  12. data/lib/brut/front_end/forms/constraint_violation.rb +2 -18
  13. data/lib/brut/front_end/forms/input.rb +8 -8
  14. data/lib/brut/front_end/forms/input_declarations.rb +3 -3
  15. data/lib/brut/front_end/forms/radio_button_group_input.rb +1 -1
  16. data/lib/brut/front_end/forms/select_input.rb +1 -1
  17. data/lib/brut/front_end/forms/validity_state.rb +23 -0
  18. data/lib/brut/spec_support/handler_support.rb +1 -1
  19. data/lib/brut/spec_support/matcher.rb +1 -1
  20. data/lib/brut/spec_support/matchers/be_a_bug.rb +11 -0
  21. data/lib/brut/spec_support/matchers/be_page_for.rb +10 -0
  22. data/lib/brut/spec_support/matchers/be_routing_for.rb +18 -0
  23. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +10 -0
  24. data/lib/brut/spec_support/matchers/have_generated.rb +28 -0
  25. data/lib/brut/spec_support/matchers/have_html_attribute.rb +10 -0
  26. data/lib/brut/spec_support/matchers/have_i18n_string.rb +13 -0
  27. data/lib/brut/spec_support/matchers/have_link_to.rb +15 -0
  28. data/lib/brut/spec_support/matchers/have_redirected_to.rb +23 -0
  29. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +20 -0
  30. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +22 -0
  31. data/lib/brut/version.rb +1 -1
  32. metadata +3 -2
  33. data/lib/brut/spec_support/matchers/have_rendered.rb +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 327b83333a37823010e1d2763646d56bd503d505d6c965ddc5d0d8098ecd9299
4
- data.tar.gz: d12d8d46f6e459d97f3489218302da647e073ac4d8e48cf57cd1d1fee998c6a1
3
+ metadata.gz: ad36cf0ea0c628d6ab48dafc53dfb9253f19ac5df73669e86532970ea70e759d
4
+ data.tar.gz: fa4dcb2b80e205fb2de2372fe8cfdfd441b6634ab69458287e41f5e681cc507f
5
5
  SHA512:
6
- metadata.gz: a55e3607d5d127e74f93864f6dc19fad6dc2a0631e56559cf92df4b5762771d82d5bc0c5a2d289d6948a0899568e0138e503fea98f91d42007cefb9b3e83482b
7
- data.tar.gz: e901897e8d8eb2a2ef08bc4e5efacc0c3a13f6981f8523a2e0db3ade740f648923ecb29e9634ea133dd16efccedeecb0b9ec06f820c0b4d77b7f34068f96d540
6
+ metadata.gz: a357cccfd1224fcd00d7909670d026a532c1d8cdc0bdd2549a92fad1621370bbdcde707b8fdab0c003ddbe71728018aef2c45201194614025f1b97520775847e
7
+ data.tar.gz: 042fc9088726c05c0f12b573f54eeb2d7ace79a4552a5756170fb776eaddf5ad895d2a702f1b2e5f05d94f5ff07b7ca4b560e9c3729153441458aa6c3c8e2340
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brut (0.0.26)
4
+ brut (0.0.27)
5
5
  concurrent-ruby
6
6
  i18n
7
7
  irb
@@ -89,6 +89,7 @@ export default defineConfig({
89
89
  { text: "Middleware", link: "/middleware" },
90
90
  { text: "Instrumentation", link: "/instrumentation" },
91
91
  { text: "Security", link: "/security" },
92
+ { text: "LSP Support", link: "/lsp" },
92
93
  ],
93
94
  },
94
95
  {
data/brutrb.com/ai.md CHANGED
@@ -58,7 +58,7 @@ The logo is level 4 - ChatGPT created it. I'd love to have a real person make a
58
58
  something to get this launched. Please reach out if you want to make a better one + other assets. I'm
59
59
  willing to pay a real person.
60
60
 
61
- ## AI That Teachs You About Brut Does Not Currently Exist
61
+ ## AI Completions Should Be Viewed with Skepticism
62
62
 
63
63
  Due to how LLMs work, there is naturally nothing in any model about Brut. Anything an
64
64
  existing model tells you about Brut is **100% untrustworthy**. We hope to allow LLMs to consume Brut's
@@ -66,3 +66,8 @@ code, documentation, and examples, so it can be an additional source of help, bu
66
66
  the case.
67
67
 
68
68
  **Do not ask an LLM about Brut** until this part of the documentation changes.
69
+
70
+ For completion-based AI suggestions, **view them with skepticism**. In my
71
+ experience, completions from e.g. GitHub CoPilot work OK when replicating a pattern
72
+ in the file you are editing, but suggestions in a freshly-opened file tend to be
73
+ entirely imaginary or Rails-based. Beware.
@@ -136,7 +136,7 @@ To use it without having to instantiate it, call `global_component` with the com
136
136
  class HomePage < AppPage
137
137
  def page_template
138
138
  header do
139
- render global_component(FlashMessage)
139
+ global_component(FlashMessage) # note: render not required
140
140
  end
141
141
  end
142
142
  end
data/brutrb.com/forms.md CHANGED
@@ -87,7 +87,7 @@ class LoginPage < AppPage
87
87
  end
88
88
  ```
89
89
 
90
- Brut can generate the HTML for the needed inputs via `Brut::FrontEnd::Components::Inputs::TextField.for_form_input`, which is a very long name. Hold that thought for now. This method will generate an `<input>` element for you, based on how you've set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.
90
+ Brut can generate the HTML for the needed inputs via `Brut::FrontEnd::Components::Inputs::TextField`, which is a very long class name. Hold that thought for now. This method will generate an `<input>` element for you, based on how you've set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.
91
91
 
92
92
  ```ruby {11,12}
93
93
  # app/src/front_end/pages/login_page.rb
@@ -100,8 +100,8 @@ class LoginPage < AppPage
100
100
  form_tag(method: :post,
101
101
  action: LoginHandler.routing) do
102
102
  # We promise you don't have to type this every time!
103
- Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form: @form, input_name: :email)
104
- Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form: @form, input_name: :password)
103
+ Brut::FrontEnd::Components::Inputs::TextField.new(form: @form, input_name: :email)
104
+ Brut::FrontEnd::Components::Inputs::TextField.new(form: @form, input_name: :password)
105
105
  button { "Login" }
106
106
  end
107
107
  end
@@ -134,7 +134,7 @@ class LoginPage < AppPage
134
134
  end
135
135
  ```
136
136
 
137
- This allows you to call `Inputs::TextField.for_form_input`:
137
+ This allows you to call `Inputs::TextField` like a method:
138
138
 
139
139
  ```ruby {12,13}
140
140
  # app/src/front_end/pages/login_page.rb
@@ -148,8 +148,8 @@ class LoginPage < AppPage
148
148
  def page_template
149
149
  form_tag(method: :post,
150
150
  action: LoginHandler.routing) do
151
- Inputs::TextField.for_form_input(form: @form, input_name: :email)
152
- Inputs::TextField.for_form_input(form: @form, input_name: :password)
151
+ Inputs::TextField(form: @form, input_name: :email)
152
+ Inputs::TextField(form: @form, input_name: :password)
153
153
  button { "Login" }
154
154
  end
155
155
  end
@@ -217,7 +217,7 @@ with that email and password. Let's assume the existence of the class `Authorize
217
217
  If that returns `nil`, we want to re-render the `LoginPage`, exposing some sort of constraint violation message
218
218
  so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.
219
219
 
220
- `for_form_input` can handle this, so we need to pass our form object into `LoginPage` instead of allowing `LoginPage` to create an empty one. We can do that by adding a `form:` keyword argument that defaults to `nil`:
220
+ `Brut::FrontEnd::Components` can handle this, so we need to pass our form object into `LoginPage` instead of allowing `LoginPage` to create an empty one. We can do that by adding a `form:` keyword argument that defaults to `nil`:
221
221
 
222
222
  ```ruby {3,4}
223
223
  # app/src/front_end/pages/login_page.rb
@@ -229,8 +229,8 @@ class LoginPage < AppPage
229
229
  def page_template
230
230
  form_tag(method: :post,
231
231
  action: LoginHandler.routing) do
232
- Inputs::TextField.for_form_input(form: @form, input_name: :email)
233
- Inputs::TextField.for_form_input(form: @form, input_name: :password)
232
+ Inputs::TextField(form: @form, input_name: :email)
233
+ Inputs::TextField(form: @form, input_name: :password)
234
234
  button { "Login" }
235
235
  end
236
236
  end
@@ -267,12 +267,11 @@ class LoginHandler < AppHandler
267
267
  end
268
268
  ```
269
269
 
270
- When `LoginPage` generates HTML, different HTML is generated, since the form being passed to
271
- `for_form_input` contains constraint violations.
270
+ When `LoginPage` generates HTML, different HTML is generated, since the form being passed to the components contains constraint violations.
272
271
 
273
272
  #### Showing Constraint Violations in HTML
274
273
 
275
- When `Inputs::TextField.for_form_input` is called with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing `LoginPage` (again, formatted her for clarity):
274
+ When `Brut::FrontEnd::Components::Inputs::TextField` is created with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing `LoginPage` (again, formatted her for clarity):
276
275
 
277
276
  ```html {3}
278
277
  <form method="post" action="/login">
@@ -335,10 +334,10 @@ def page_template
335
334
  form_tag(method: :post,
336
335
  action: LoginHandler.routing) do
337
336
 
338
- Inputs::TextField.for_form_input(form: @form, input_name: :email)
337
+ Inputs::TextField(form: @form, input_name: :email)
339
338
  ConstraintViolations(form: @form, input_name: :email)
340
339
 
341
- Inputs::TextField.for_form_input(form: @form, input_name: :password)
340
+ Inputs::TextField(form: @form, input_name: :password)
342
341
  ConstraintViolations(form: @form, input_name: :password)
343
342
 
344
343
  button { "Login" }
@@ -419,10 +418,10 @@ def page_template
419
418
  form_tag(method: :post,
420
419
  action: LoginHandler.routing) do
421
420
 
422
- Inputs::TextField.for_form_input(form: @form, input_name: :email)
421
+ Inputs::TextField(form: @form, input_name: :email)
423
422
  ConstraintViolations(form: @form, input_name: :email)
424
423
 
425
- Inputs::TextField.for_form_input(form: @form, input_name: :password)
424
+ Inputs::TextField(form: @form, input_name: :password)
426
425
  ConstraintViolations(form: @form, input_name: :password)
427
426
 
428
427
  button { "Login" }
@@ -564,7 +563,7 @@ class LoginForm < AppForm
564
563
  end
565
564
  ```
566
565
 
567
- Checkboxes can be rendered by `Inputs::TextField.for_form_input`, and their `value` attribute would always be the string `"true"`. If the form's value for the input is the string `"true"`, the checkbox would have the `checked` attribute:
566
+ Checkboxes can be rendered by a `Brut::FrontEnd::Components::Inputs::TextField`, and their `value` attribute would always be the string `"true"`. If the form's value for the input is the string `"true"`, the checkbox would have the `checked` attribute:
568
567
 
569
568
  ```html
570
569
  <!-- Form.new(params: { remember: "true" }) -->
@@ -578,8 +577,7 @@ Radio buttons are implemented in HTML by `<input type="radio">`, with an expecta
578
577
  having the same value for the `name` attribute, but different values for the `value` attributes, one of which may
579
578
  be `checked`.
580
579
 
581
- Brut implements this via `Brut::FrontEnd::Components::Inputs::RadioButton`, which has the class method
582
- `for_form_input`. To create radio buttons in a form, use `radio_button_group`:
580
+ Brut implements this via `Brut::FrontEnd::Components::Inputs::RadioButton`, whose initializer behaves like the other form input components. To create radio buttons in a form, use `radio_button_group`:
583
581
 
584
582
  ```ruby {5}
585
583
  # app/src/front_end/forms/login_form.rb
@@ -598,7 +596,7 @@ def view_template
598
596
  [ :never, :one_week, :one_month ].each do |remember|
599
597
  label do
600
598
  render(
601
- Inputs::RadioButton.for_form_input(
599
+ Inputs::RadioButton(
602
600
  form:,
603
601
  input_name: :remember,
604
602
  value: remember
@@ -643,8 +641,7 @@ class LoginForm < AppForm
643
641
  end
644
642
  ```
645
643
 
646
- Creating the HTML can be done with `Brut::FrontEnd::Components::Inputs::Select`. It's `for_form_input` is more complex, since it provides a way to show visitor-friendly values instead of the innate `value` for each option,
647
- as well as to allow for a "blank" entry.
644
+ Creating the HTML can be done with `Brut::FrontEnd::Components::Inputs::Select`. It's initializer is more complex, since it provides a way to show visitor-friendly values instead of the innate `value` for each option, as well as to allow for a "blank" entry.
648
645
 
649
646
  Let's suppose we have a class named `LoginRememberOption`. It's a simple wrapper around a value we might store in the database and use to lookup an I18n key.
650
647
 
@@ -677,7 +674,7 @@ To show these options in a `<select>`, we might do this:
677
674
  def view_template
678
675
  form do
679
676
  render(
680
- Inputs::Select.for_form_input(
677
+ Inputs::Select(
681
678
  form:,
682
679
  input_name: :remember,
683
680
  options: LoginRememberOption.all,
@@ -718,8 +715,7 @@ end
718
715
 
719
716
  In this case, we need `required: false` or every single field we generate will be required.
720
717
 
721
- To generate the HTML, use the optional `index:` parameter to `for_form_input` as well as for
722
- `ConstraintViolations`:
718
+ To generate the HTML, use the optional `index:` parameter to the initializer as well as for `ConstraintViolations`:
723
719
 
724
720
  ```ruby {11,16}
725
721
  # Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb
@@ -729,7 +725,7 @@ def page_template
729
725
  action: BulkWidgetForm.routing) do
730
726
 
731
727
  10.times do |i|
732
- Inputs::TextField.for_form_input(
728
+ Inputs::TextField(
733
729
  form: @form,
734
730
  input_name: :name,
735
731
  index: i
@@ -1,46 +1,51 @@
1
1
  # Getting Started
2
2
 
3
- To make a new app with Brut, you'll need to clone a template app, initialize it, then set up your dev environment.
3
+ Brut is developed alongside a separate gem called `mkbrut`, which allows you to
4
+ create a new Brut app. It will set up you dev environment as well.
4
5
 
5
- ## Clone the App Template
6
+ ## Get `mkbrut`
6
7
 
7
- To get started, clone the Brut app template:
8
+ If you have a Ruby 3.4 (or later) environment set up on your computer, you can use
9
+ RubyGems:
8
10
 
9
11
  ```
10
- > git clone https://github.com/thirdtank/brut-app-template your-app-name
12
+ gem install mkbrut
13
+ ```
14
+
15
+ If not, we recommend you use a pre-built Docker image:
16
+
17
+ ```
18
+ docker pull XXXX
11
19
  ```
12
20
 
13
21
  ## Init Your App
14
22
 
15
- The template includes `init`, which will ask you a few questions to get everything set up.
23
+ A Brut app just needs a name, which will be used to derive a few more useful values.
24
+ For now:
16
25
 
17
26
  ```
18
- > cd your-app-name
19
- > ./init
27
+ mkbrut my-new-app
20
28
  ```
21
29
 
22
- You'll need to provide four pieces of info:
30
+ This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components. However, if you just want to skip all that:
23
31
 
24
- * Your app's name, suitable has a hostname or identifier
25
- * A prefix for your app's externalizable ids
26
- * A prefix for your app's custom elements
27
- * An organization name, needed for deployment
28
-
29
- ::: tip
30
- Choose your app's name wisely, however everything else can be easily changed later, so don't stress!
31
- :::
32
+ ```
33
+ mkbrut --no-demo my-new-app
34
+ ```
32
35
 
33
- ## Set Up Your Dev Environment
36
+ ## Start Your Dev Environment
34
37
 
35
- Brut includes a dev environment based on Docker.
38
+ Brut includes a dev environment based on Docker. It uses Docker compose to run a
39
+ Docker container where your app will run, a Docker container for Postgres, and a
40
+ Docker container for local observability via OpenTelemetry.
36
41
 
37
42
  1. [Install Docker](https://docs.docker.com/get-started/get-docker/)
38
- 2. Build Your images
43
+ 2. Build the image used to create you app's container:
39
44
 
40
45
  ```
41
46
  > dx/build
42
47
  ```
43
- 3. Start up the environment
48
+ 3. Start up all the containers:
44
49
 
45
50
  ```
46
51
  > dx/start
@@ -51,6 +56,13 @@ Brut includes a dev environment based on Docker.
51
56
  > dx/exec bin/setup
52
57
  ```
53
58
 
59
+ OR:
60
+
61
+ ```
62
+ > dx/exec bash
63
+ inside-container> bin/setup
64
+ ```
65
+
54
66
  Now, you're ready to go
55
67
 
56
68
  ## Run the App
@@ -59,8 +71,31 @@ Now, you're ready to go
59
71
  > dx/exec bin/dev
60
72
  ```
61
73
 
74
+ OR
75
+
76
+ ```
77
+ > dx/exec bash
78
+ > bin/dev
79
+ ```
80
+
62
81
  You can now visit your app at `localhost:6502`
63
82
 
83
+ ## Run the Tests
84
+
85
+ Even without the demo, there are a few components set up, and there are some tests:
86
+
87
+ ```
88
+ > dx/exec bin/ci
89
+ ```
90
+
91
+ OR
92
+
93
+ ```
94
+ > dx/exec bash
95
+ > bin/ci
96
+ ```
97
+
64
98
  ## Now Build The Rest of Your App 🦉
65
99
 
66
- You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the API docs.
100
+ You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the API docs. You might also want to check out the docs for [LSP Support](/lsp).
101
+
data/brutrb.com/lsp.md ADDED
@@ -0,0 +1,23 @@
1
+ # Language Server Protocol (LSP) Support
2
+
3
+ Because Brut development happens inside Docker, but your editor likely runs on your
4
+ computer, getting LSP servers running takes a few more steps.
5
+
6
+ ## Overview
7
+
8
+ When you created your app with `mkbrut`, the following LSP-related modules are set
9
+ up and/or installed:
10
+
11
+ * Shopify's Ruby LSP server (installed from `bin/setup`)
12
+ * Microsoft's TypeScript/JavaScript and CSS LSP serfvers (specified in `package.json`, installed when `npm install` runs from `bin/setup`)
13
+
14
+ In order to use them from your computer a few configurations are needed, some of
15
+ which Brut has done, and some you will need to do.
16
+
17
+ | Configuration | Description | Brut Handled? |
18
+ |---|---|---|
19
+ | Paths inside Docker Must Match Your Computer | When an LSP server communicates about a file, it does so with a path. That means that paths inside the Docker container must be the same as those on your computer. Brut achievecs this by using `${CWD}` inside `docker-compose.dx.yml` | ✅ |
20
+ | Third party libraries must *also* be installed in a path that is the same in both places | When jumping to a definition, the LSP server will again use paths, which must match. Because Node modules are installed local to your app, they already work. Ruby Gems, however, are configured to be installed in `local-gems` in your app. Brut should've added this to `.gitignore` and setup everything inside Docker to use it. | ✅ |
21
+ | Your editor must use `dx/exec` to execute LSP commands | Your editor will need to know that the LSP servers are running inside Docker. If your editor allows configuring the commands used to do this, you must prefix them with `dx/exec bashc -lc`. See [my blog post](https://naildrivin5.com/blog/2025/06/12/neovim-and-lsp-servers-working-with-docker-based-development.html) for details. | ❌ |
22
+ | Other languages or plugins to existing LSP servers | I haven't used these, so no idea how well they work. | ❌ |
23
+
@@ -133,7 +133,8 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
133
133
  test_expected: true,
134
134
  }
135
135
  if pathname.fnmatch?( (Brut.container.components_src_dir / "**").to_s )
136
- if pathname.basename.to_s == "app_component.rb"
136
+ if pathname.basename.to_s == "app_component.rb" ||
137
+ pathname.basename.to_s == "custom_element_registration.rb"
137
138
  hash[:type] = :infrastructure
138
139
  hash[:test_expected] = false
139
140
  else
@@ -1,6 +1,22 @@
1
1
  require "phlex"
2
2
 
3
- # Components holds Brut-provided components that are of general use to any web app
3
+ # Namespace for Brut-provided components that are of general use to any web app.
4
+ # Also extends [`Phlex:::Kit`](https://www.phlex.fun/components/kits.html), meaning
5
+ # you can include this module in your pages and components to be able to
6
+ # create Brut's components without `.new` or without the full classname:
7
+ #
8
+ # @example
9
+ # class AppPage < Brut::FrontEnd::Page
10
+ # include Brut::FrontEnd::Components
11
+ # end
12
+ #
13
+ # class HomePage < AppPage
14
+ # def page_template
15
+ # h1 do
16
+ # span { "It's }
17
+ # TimeTag(timestamp: Time.now)
18
+ # end
19
+ # end
4
20
  module Brut::FrontEnd::Components
5
21
  autoload(:FormTag,"brut/front_end/components/form_tag")
6
22
  autoload(:Input,"brut/front_end/components/input")
@@ -72,16 +88,24 @@ class Brut::FrontEnd::Component < Phlex::HTML
72
88
  }
73
89
  end
74
90
 
75
- # Return a component that you would like Brut to instantiate.
76
- # This will use keyword injection to create the component, which means that if the component
77
- # doesn't require any data from this component, you do not need to pass through those values.
78
- # For example, you may have a component that renders the flash message. To avoid requiring your component to
79
- # be passed the flash, a global component can be injected with it from Brut.
91
+ # Render (in Phlex parlance) a component that you would like Brut to
92
+ # instantiate. This is useful when you want to use a component that
93
+ # only requires values from the {Brut::FrontEnd::RequestContext}. By
94
+ # using this method, *this* component does not have to receive
95
+ # data from the {Brut::FrontEnd::RequestContext} that only serves to pass
96
+ # to the component you use here.
97
+ #
98
+ # For example, you may have a component that renders the flash message. To avoid requiring *this* component/page to be passed the flash, a global component can be injected with it from Brut.
99
+ #
100
+ # This component *will* be rendered into the Phlex context. Do not call
101
+ # `render` on the result, nor rely on the return value.
80
102
  #
81
- # @return [Object] instance of `component_klass`, as created by Brut. This will
82
- # not render the component.
103
+ # @param [Class] component_klass the component class to use in the view.
104
+ # This class's
105
+ # initializer must only require information available from the
106
+ # {Brut::FrontEnd::RequestContext}.
83
107
  def global_component(component_klass)
84
- Brut::FrontEnd::RequestContext.inject(component_klass)
108
+ render Brut::FrontEnd::RequestContext.inject(component_klass)
85
109
  end
86
110
  end
87
111
  include Helpers
@@ -27,14 +27,14 @@ class Brut::FrontEnd::Components::Inputs::SelectTagWithOptions < Brut::FrontEnd:
27
27
  # to be used as the `value` attribute and option text content, respectively.
28
28
  #
29
29
  # @return [Brut::FrontEnd::Components::Inputs::SelectTagWithOptions] the select input ready to be placed into a view.
30
- def self.for_form_input(form:,
31
- input_name:,
32
- options:,
33
- include_blank: false,
34
- value_attribute:,
35
- option_text_attribute:,
36
- index: nil,
37
- html_attributes: {})
30
+ def initialize(form:,
31
+ input_name:,
32
+ options:,
33
+ include_blank: false,
34
+ value_attribute:,
35
+ option_text_attribute:,
36
+ index: nil,
37
+ html_attributes: {})
38
38
  html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
39
39
  default_html_attributes = {}
40
40
  index ||= 0
@@ -54,84 +54,26 @@ class Brut::FrontEnd::Components::Inputs::SelectTagWithOptions < Brut::FrontEnd:
54
54
  input.name
55
55
  end
56
56
 
57
- Brut::FrontEnd::Components::Inputs::SelectTagWithOptions.new(
58
- name: name,
59
- options:,
60
- selected_value: input.value,
61
- value_attribute:,
62
- option_text_attribute:,
63
- include_blank:,
64
- html_attributes: default_html_attributes.merge(html_attributes)
65
- )
66
- end
57
+ input_value = input.value
67
58
 
68
- # Create the element.
69
- #
70
- # @param [String] name the name of the input
71
- # @param [Array<Object>] options An array of objects represented what is being selected.
72
- # These can be any object and are ideally whatever domain object or
73
- # data type you want on the backend to represent this selection.
74
- # @param [Symbol|String] value_attribute the name of an attribute to determine an option's value.
75
- # This will be called on each element of `options` to get the value used for the `<option>`'s
76
- # `value` attribute. The value returned by `value_attribute` should be unique amongst the
77
- # `options` provided *and* be distinct from whatever `value` is used for `include_blank`.
78
- # @param [String] selected_value the value of the selected option. Note that this is the *value*
79
- # of the selected option, not the selected option itself. To set the selected value
80
- # based on a selected option, omit this and use `selected_option`
81
- # @param [String] selected_option the selected option. Note that `value_attribute` will be called
82
- # on this to determine the selected value to use when generating HTML. Also note that
83
- # this object must be in `options` or an exeception is raised.
84
- # @param [Symbol|String] option_text_attribute the name of an attribute to determine the text for an option.
85
- # This will be called on each element of `options` to get the value used for the `<option>`'s
86
- # text content. The value returned by `option_text_attribute` need not be unique, though if it
87
- # is not unique, it will certainly be confusing.
88
- # @param [Hash] html_attributes any additional HTML attributes to include on the `<select>` element.
89
- # @param [false|true|Hash] include_blank configure how and if to include a blank element in the select.
90
- # If this is false, there will be no blank element. If it's `true`, there will be one with
91
- # no value or text. If this is a `Hash` it must contain a `value:` key and a `text_content:` key
92
- # to be used as the `value` attribute and option text content, respectively.
93
- #
94
- # @raise ArgumentError if `selected_option` is present, but not in `options` or if `selected_value` is
95
- # present, but no option's value for `value_attribute` is that value.
96
- #
97
- # XXX: Why does this not ask the form for the selected_value?
98
- # XXX: This doesn't do well when values are strings
99
- def initialize(name:,
100
- options:,
101
- value_attribute:,
102
- selected_value: nil,
103
- selected_option: nil,
104
- option_text_attribute:,
105
- include_blank: false,
106
- html_attributes:)
107
59
  @options = options
108
60
  @include_blank = IncludeBlank.from_param(include_blank)
109
61
  @value_attribute = value_attribute
110
62
  @option_text_attribute = option_text_attribute
111
- @html_attributes = html_attributes
63
+ @html_attributes = default_html_attributes.merge(html_attributes)
112
64
  @html_attributes[:name] = name
113
65
 
114
- if selected_value.nil?
115
- if selected_option.nil?
116
- @selected_value = nil # explicitly nothing is selected
117
- else
118
- option = options.detect { |option|
119
- option.send(@value_attribute) == selected_option.send(@value_attribute)
120
- }
121
- if option.nil?
122
- raise ArgumentError, "selected_option #{selected_option} (with #{value_attribute} '#{selected_option.send(value_attribute)}') was not found in options"
123
- end
124
- @selected_value = option.send(@value_attribute)
125
- end
66
+ if input_value.nil?
67
+ @selected_value = nil # explicitly nothing is selected
126
68
  else
127
- if selected_value.kind_of?(Array)
128
- raise "WTF: #{name}"
69
+ if input_value.kind_of?(Array)
70
+ raise "WTF: #{name}" # XXX?
129
71
  end
130
72
  option = options.detect { |option|
131
- selected_value == option.send(@value_attribute)
73
+ input_value == option.send(@value_attribute)
132
74
  }
133
75
  if option.nil?
134
- raise ArgumentError, "selected_value #{selected_value} was not the value for #{value_attribute} on any of the options: #{options.map { |option| option.send(value_attribute) }.join(', ')}"
76
+ raise ArgumentError, "selected_value #{input_value} was not the value for #{value_attribute} on any of the options: #{options.map { |option| option.send(value_attribute) }.join(', ')}"
135
77
  end
136
78
  @selected_value = option.send(@value_attribute)
137
79
  end
@@ -2,22 +2,6 @@
2
2
  # form-related classes.
3
3
  class Brut::FrontEnd::Forms::ConstraintViolation
4
4
 
5
- # These are underscorized versions of the attributes of the browser's `ValidityState`'s properties.
6
- #
7
- # @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
8
- CLIENT_SIDE_KEYS = [
9
- "bad_input",
10
- "custom_error",
11
- "pattern_mismatch",
12
- "range_overflow",
13
- "range_underflow",
14
- "step_mismatch",
15
- "too_long",
16
- "too_short",
17
- "type_mismatch",
18
- "value_missing",
19
- ]
20
-
21
5
  # @return [String] the key fragment representing the violation
22
6
  attr_reader :key
23
7
  # @return [Hash] interpolated values useful in rendering the actual message
@@ -27,11 +11,11 @@ class Brut::FrontEnd::Forms::ConstraintViolation
27
11
  #
28
12
  # @param [String|Symbol] key I18n key fragment representing this violation.
29
13
  # @param [Hash|nil] context interpolated values useful in rendering the message
30
- # @param [true|:based_on_key] server_side If `:based_on_key`, {#client_side?} will return true if `key` is in {.CLIENT_SIDE_KEYS}.
14
+ # @param [true|:based_on_key] server_side If `:based_on_key`, {#client_side?} will return true if `key` is in {ValidityState.KEYS}.
31
15
  # If `true`, {#client_side?} will return false no matter what.
32
16
  def initialize(key:,context:, server_side: :based_on_key)
33
17
  @key = key.to_s
34
- @client_side = CLIENT_SIDE_KEYS.include?(@key) && server_side != true
18
+ @client_side = Brut::FrontEnd::Forms::ValidityState::KEYS.include?(@key) && server_side != true
35
19
  @context = context || {}
36
20
  if !@context.kind_of?(Hash)
37
21
  raise "#{self.class} created for key #{key} with an invalid context: '#{context}/#{context.class}'. Context must be nil or a hash"
@@ -82,14 +82,14 @@ class Brut::FrontEnd::Forms::Input
82
82
  step_mismatch = false
83
83
 
84
84
  @validity_state = Brut::FrontEnd::Forms::ValidityState.new(
85
- value_missing: missing,
86
- too_short: too_short,
87
- too_long: too_short,
88
- range_overflow: range_overflow,
89
- range_underflow: range_underflow,
90
- pattern_mismatch: pattern_mismatch,
91
- step_mismatch: step_mismatch,
92
- type_mismatch: type_mismatch,
85
+ valueMissing: missing,
86
+ tooShort: too_short,
87
+ tooLong: too_short,
88
+ rangeOverflow: range_overflow,
89
+ rangeUnderflow: range_underflow,
90
+ patternMismatch: pattern_mismatch,
91
+ stepMismatch: step_mismatch,
92
+ typeMismatch: type_mismatch,
93
93
  )
94
94
  @value = new_value
95
95
  end
@@ -12,8 +12,8 @@ module Brut::FrontEnd::Forms::InputDeclarations
12
12
  end
13
13
 
14
14
  # Declares a select for this form, to be modeled via an HTML `<SELECT>` tag. Note that this will not define the values that appear
15
- # in the select. That is done when the select is rendered, which you might do with
16
- # {Brut::FrontEnd::Components::Inputs::Select.for_form_input}
15
+ # in the select. That is done when the select is rendered, which you might do with a
16
+ # {Brut::FrontEnd::Components::Inputs::Select}
17
17
  #
18
18
  # @param [String] name The name of the input (used in the `name` attribute)
19
19
  # @param [Hash] attributes Attributes to be used on the tag that represent its contraints. See {Brut::FrontEnd::Forms::SelectInputDefinition}
@@ -28,7 +28,7 @@ module Brut::FrontEnd::Forms::InputDeclarations
28
28
  # input tags.
29
29
  #
30
30
  # Note that this is not where you would define the possible values for the group. That is done in
31
- # {Brut::FrontEnd::Components::Inputs::RadioButton.for_form_input}.
31
+ # {Brut::FrontEnd::Components::Inputs::RadioButton}.
32
32
  #
33
33
  # @param [String] name The name of the group (used in the `name` attribute)
34
34
  # @param [Hash] attributes Attributes to be used on the tag that represent its contraints. See {Brut::FrontEnd::Forms::RadioButtonGroupInputDefinition}
@@ -32,7 +32,7 @@ class Brut::FrontEnd::Forms::RadioButtonGroupInput
32
32
  end
33
33
 
34
34
  @validity_state = Brut::FrontEnd::Forms::ValidityState.new(
35
- value_missing: missing,
35
+ valueMissing: missing,
36
36
  )
37
37
  @value = new_value
38
38
  end
@@ -30,7 +30,7 @@ class Brut::FrontEnd::Forms::SelectInput
30
30
  end
31
31
 
32
32
  @validity_state = Brut::FrontEnd::Forms::ValidityState.new(
33
- value_missing: missing,
33
+ valueMissing: missing,
34
34
  )
35
35
  @value = new_value
36
36
  end
@@ -3,6 +3,11 @@
3
3
  # In a sense, this is a wrapper for one or more {Brut::FrontEnd::Forms::ConstraintViolation} instances in the
4
4
  # context of an input.
5
5
  #
6
+ # This class also holds the logic related to client- vs. server-side constraint
7
+ # violations. As such, {.KEYS} is the list of known client-side
8
+ # constraint violation keys, as defined by browser's `ValidityState` class. These
9
+ # are left as camel-case to make this clear.
10
+ #
6
11
  # @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
7
12
  class Brut::FrontEnd::Forms::ValidityState
8
13
  include Enumerable
@@ -46,5 +51,23 @@ class Brut::FrontEnd::Forms::ValidityState
46
51
  end
47
52
  end
48
53
 
54
+
55
+ # These are the attributes of the browser's `ValidityState`'s properties.
56
+ # They are left as camel-case to clearly indicate they come from the browser.
57
+ #
58
+ # @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
59
+ KEYS = [
60
+ "badInput",
61
+ "customError",
62
+ "patternMismatch",
63
+ "rangeOverflow",
64
+ "rangeUnderflow",
65
+ "stepMismatch",
66
+ "tooLong",
67
+ "tooShort",
68
+ "typeMismatch",
69
+ "valueMissing",
70
+ ]
71
+
49
72
  end
50
73
 
@@ -6,7 +6,7 @@ require_relative "session_support"
6
6
  #
7
7
  #
8
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
9
+ # * `have_generated` to check that the handler rendered a specific page
10
10
  # * `have_returned_http_status` to check that the handler returned an HTTP status
11
11
  module Brut::SpecSupport::HandlerSupport
12
12
  include Brut::SpecSupport::FlashSupport
@@ -8,7 +8,7 @@ require_relative "matchers/have_constraint_violation"
8
8
  require_relative "matchers/have_html_attribute"
9
9
  require_relative "matchers/have_i18n_string"
10
10
  require_relative "matchers/have_redirected_to"
11
- require_relative "matchers/have_rendered"
11
+ require_relative "matchers/have_generated"
12
12
  require_relative "matchers/have_returned_http_status"
13
13
  require_relative "matchers/have_returned_rack_response"
14
14
  require_relative "matchers/have_link_to"
@@ -12,3 +12,14 @@ RSpec::Matchers.define :be_a_bug do
12
12
 
13
13
  supports_block_expectations
14
14
  end
15
+
16
+ # Block-based matcher that expects the blog to
17
+ # raise a {Brut::Framework::Errors::Bug}.
18
+ #
19
+ # @example
20
+ # expect {
21
+ # SomeClass.new(value: :invalid)
22
+ # }.to be_a_bug
23
+ #
24
+ class Brut::SpecSupport::Matchers::BeABug
25
+ end
@@ -14,3 +14,13 @@ RSpec::Matchers.define :be_page_for do |klass|
14
14
  end
15
15
  end
16
16
  end
17
+
18
+ # Matcher for end-to-end tests to assert that the current page
19
+ # is the page you expect it to be. This is based on the
20
+ # {Brut::FrontEnd::Components::PageIdentifier} component, which must
21
+ # be included on the page for this matcher to work.
22
+ #
23
+ # @example
24
+ # expect(page).to be_page_for(HomePage)
25
+ class Brut::SpecSupport::Matchers::BePageFor
26
+ end
@@ -9,3 +9,21 @@ RSpec::Matchers.define :be_routing_for do |klass,**args|
9
9
  end
10
10
 
11
11
  end
12
+
13
+ # Matcher used for handlers (or any code that returns a `URI`)
14
+ # to assert that the URI is for a given page with the given set of parameters
15
+ #
16
+ # @example
17
+ # result = handler.handle
18
+ # expect(result).to be_routing_for(HomePage)
19
+ #
20
+ # @example with parameters
21
+ # result = handler.handle
22
+ # expect(result).to be_routing_for(WidgetsByWidgetIdPage, id: widget.external_id)
23
+ #
24
+ # @example with anchor
25
+ # result = handler.handle
26
+ # expect(result).to be_routing_for(MessagesPage, anchor: "latest_message")
27
+ #
28
+ class Brut::SpecSupport::Matchers::BeRoutingFor
29
+ end
@@ -29,6 +29,16 @@ RSpec::Matchers.define :have_constraint_violation do |field,key:,index:nil|
29
29
  end
30
30
  end
31
31
 
32
+ # Matcher to check that a from has a specific constraint violation.
33
+ #
34
+ # @example
35
+ # expect(form).to have_constraint_violation(:email, key: :required)
36
+ #
37
+ # @example Index fields (requires that the third email field have a constraint violation)
38
+ # expect(form).to have_constraint_violation(:email, key: :required, index: 2)
39
+ #
40
+ # @example Negated
41
+ # expect(form).not_to have_constraint_violation(:email, key: :required)
32
42
  class Brut::SpecSupport::Matchers::HaveConstraintViolation
33
43
  attr_reader :fields_found
34
44
  attr_reader :keys_on_field_found
@@ -0,0 +1,28 @@
1
+ # Handler
2
+ RSpec::Matchers.define :have_generated do |component_or_page|
3
+ match do |result|
4
+ result.class.ancestors.include?(component_or_page)
5
+ end
6
+
7
+ failure_message do |result|
8
+ "Expected a #{component_or_page} to be generated, but got #{result}"
9
+ end
10
+ failure_message_when_negated do |result|
11
+ "Got #{component_or_page} when not expected"
12
+ end
13
+
14
+ end
15
+
16
+ # Matcher to check that a handler generated a specific page
17
+ # (as opposed to redirected to a page). This works for components
18
+ # as well, in the case of Ajax requests.
19
+ #
20
+ # @example
21
+ # result = handler.handle(form:)
22
+ # expect(result).to have_generated(NewWidgetPage)
23
+ #
24
+ # @example Ajax request
25
+ # result = handler.handle(form:, xhr: true)
26
+ # expect(result).to have_generated(WidgetResponseComponent)
27
+ class Brut::SpecSupport::Matchers::HaveGenerated
28
+ end
@@ -25,6 +25,16 @@ RSpec::Matchers.define :have_html_attribute do |attribute|
25
25
  end
26
26
  end
27
27
 
28
+ # Used in component or page specs to check that a given node
29
+ # has a particular HTML attribnute, or an attribute with a speecific value.
30
+ #
31
+ # @example
32
+ # result = generate_and_parse(page)
33
+ # expect(result.e!("blockquote")).to have_html_attribute(:id)
34
+ #
35
+ # @example Expecting a value as well
36
+ # result = generate_and_parse(page)
37
+ # expect(result.e!("blockquote")).to have_html_attribute(id: "foo")
28
38
  class Brut::SpecSupport::Matchers::HaveHTMLAttribute
29
39
 
30
40
  attr_reader :error
@@ -24,3 +24,16 @@ RSpec::Matchers.define :have_i18n_string do |key,**args|
24
24
  end
25
25
  end
26
26
 
27
+ # Used in component or page specs to check if a Nokogiri node's
28
+ # text contains a specific i18n string, without you having
29
+ # to use `t` to look it up.
30
+ #
31
+ # @example
32
+ # result = generate_and_parse(page)
33
+ # expect(result.e!("h3")).to have_i18n_string(:greeting)
34
+ #
35
+ # @example I18n string with parameters
36
+ # result = generate_and_parse(page)
37
+ # expect(result.e!("h3")).to have_i18n_string(:user_greeting, email: account.email)
38
+ class Brut::SpecSupport::Matchers::HaveI18nString
39
+ end
@@ -13,3 +13,18 @@ RSpec::Matchers.define :have_link_to do |page_klass,**args|
13
13
  "Did not expect to find link to #{page_klass.routing(**args)}."
14
14
  end
15
15
  end
16
+
17
+ # Used on a component/page spec to check that there is a link
18
+ # to a specific routing. This handles creating a CSS selector
19
+ # like `[href="#{page_klass.routing(**args)}"]`.
20
+ #
21
+ # @example
22
+ # result = generate_and_parse(page)
23
+ # expect(result.e!("nav")).to have_link_to(HomePage)
24
+ #
25
+ # @example link with parameters
26
+ # result = generate_and_parse(page)
27
+ # expect(result.e!("nav")).to have_link_to(WidgetsByWidgetIdPage, id: widget.id)
28
+ #
29
+ class Brut::SpecSupport::Matchers::HaveLinkTo
30
+ end
@@ -37,5 +37,28 @@ RSpec::Matchers.define :have_redirected_to do |page_or_uri,**page_params|
37
37
  failure_message_when_negated do |result|
38
38
  "Got a redirect when it wasn't expected"
39
39
  end
40
+ end
40
41
 
42
+ # Used on handler specs to check that a response has
43
+ # redirected to a page or URI. Can also be used
44
+ # with {Brut::SpecSupport::ComponentSupport#generate_result} to
45
+ # check that a Page's {Brut::FrontEnd::Page#before_generate} method
46
+ # did what you expect
47
+ #
48
+ # @example
49
+ # result = handler.handle
50
+ # expect(result).to have_redirected_to(HomePage)
51
+ #
52
+ # @example with parameters
53
+ # result = handler.handle
54
+ # expect(result).to have_redirected_to(WidgetsByWidgetIdPage, id: widget.id)
55
+ #
56
+ # @example arbitrary URI
57
+ # result = handler.handle
58
+ # expect(result).to have_redirected_to("http://kagi.com")
59
+ #
60
+ # @example testing a `before_generate` method
61
+ # result = generate_result(page)
62
+ # expect(result).to have_redirected_to(LoginPage)
63
+ class Brut::SpecSupport::Matchers::HaveRedirectedTo
41
64
  end
@@ -31,5 +31,25 @@ RSpec::Matchers.define :have_returned_http_status do |http_status=nil|
31
31
  "Got #{http_status} when not expected (#{result.class} was returned)"
32
32
  end
33
33
  end
34
+ end
34
35
 
36
+ # Used on handler specs to check that a response returned
37
+ # a particular HTTP status code. Can also be used
38
+ # with {Brut::SpecSupport::ComponentSupport#generate_result} to
39
+ # check that a Page's {Brut::FrontEnd::Page#before_generate} method
40
+ # did what you expect
41
+ #
42
+ # @example
43
+ # result = handler.handle
44
+ # expect(result).to have_returned_http_status(404)
45
+ #
46
+ # @example dont' care what status, just that one was returned instead of a page generation
47
+ # result = handler.handle
48
+ # expect(result).to have_returned_http_status
49
+ #
50
+ #
51
+ # @example testing a `before_generate` method
52
+ # result = generate_result(page)
53
+ # expect(result).to have_returned_http_status(403)
54
+ class Brut::SpecSupport::Matchers::HaveReturnedHttpStatus
35
55
  end
@@ -42,3 +42,25 @@ RSpec::Matchers.define :have_returned_rack_response do |http_status: :any, heade
42
42
  end
43
43
 
44
44
  end
45
+
46
+ # Used on handler specs to check that a response returned
47
+ # a Rack response. Can also be used
48
+ # with {Brut::SpecSupport::ComponentSupport#generate_result} to
49
+ # check that a Page's {Brut::FrontEnd::Page#before_generate} method
50
+ # did what you expect
51
+ #
52
+ # The matcher expects these keyword arguments:
53
+ #
54
+ # * `http_status:` - the expected HTTP status code as a number, or `:any` (the default), if it's not relevant to the test.
55
+ # * `headers:` - the expected headers as a Hash of Strings to Strings, or `:any` (the default), if they are not relevant to the test.
56
+ # * `body:` - the expected body, or `:any` (the default), if it is not relevant to the test.
57
+ #
58
+ # @example
59
+ # result = handler.handle
60
+ # expect(result).to have_returned_rack_response(
61
+ # http_status: 200,
62
+ # headers: { "Content-Type" => "text/html" }
63
+ # )
64
+ #
65
+ class Brut::SpecSupport::Matchers::HaveReturnedRackResponse
66
+ end
data/lib/brut/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Brut
2
2
  # @!visibility private
3
- VERSION = "0.0.26"
3
+ VERSION = "0.0.27"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.26
4
+ version: 0.0.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Bryant Copeland
@@ -614,6 +614,7 @@ files:
614
614
  - brutrb.com/javascript.md
615
615
  - brutrb.com/jobs.md
616
616
  - brutrb.com/keyword-injection.md
617
+ - brutrb.com/lsp.md
617
618
  - brutrb.com/markdown-examples.md
618
619
  - brutrb.com/middleware.md
619
620
  - brutrb.com/not-released.md
@@ -1182,11 +1183,11 @@ files:
1182
1183
  - lib/brut/spec_support/matchers/be_page_for.rb
1183
1184
  - lib/brut/spec_support/matchers/be_routing_for.rb
1184
1185
  - lib/brut/spec_support/matchers/have_constraint_violation.rb
1186
+ - lib/brut/spec_support/matchers/have_generated.rb
1185
1187
  - lib/brut/spec_support/matchers/have_html_attribute.rb
1186
1188
  - lib/brut/spec_support/matchers/have_i18n_string.rb
1187
1189
  - lib/brut/spec_support/matchers/have_link_to.rb
1188
1190
  - lib/brut/spec_support/matchers/have_redirected_to.rb
1189
- - lib/brut/spec_support/matchers/have_rendered.rb
1190
1191
  - lib/brut/spec_support/matchers/have_returned_http_status.rb
1191
1192
  - lib/brut/spec_support/matchers/have_returned_rack_response.rb
1192
1193
  - lib/brut/spec_support/rspec_setup.rb
@@ -1,14 +0,0 @@
1
- # Handler
2
- RSpec::Matchers.define :have_rendered do |component_or_page|
3
- match do |result|
4
- result.class.ancestors.include?(component_or_page)
5
- end
6
-
7
- failure_message do |result|
8
- "Expected a #{component_or_page} to be rendered, but got #{result}"
9
- end
10
- failure_message_when_negated do |result|
11
- "Got #{component_or_page} when not expected"
12
- end
13
-
14
- end