mkbrut 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +51 -0
- data/exe/mkbrut +5 -0
- data/lib/mkbrut/app.rb +67 -0
- data/lib/mkbrut/app_id.rb +8 -0
- data/lib/mkbrut/app_name.rb +29 -0
- data/lib/mkbrut/app_options.rb +29 -0
- data/lib/mkbrut/base.rb +57 -0
- data/lib/mkbrut/cli.rb +91 -0
- data/lib/mkbrut/erb_binding_delegate.rb +20 -0
- data/lib/mkbrut/internet_identifier.rb +32 -0
- data/lib/mkbrut/invalid_identifier.rb +4 -0
- data/lib/mkbrut/ops/add_css_import.rb +42 -0
- data/lib/mkbrut/ops/add_i18n_message.rb +74 -0
- data/lib/mkbrut/ops/add_method.rb +48 -0
- data/lib/mkbrut/ops/append_to_file.rb +17 -0
- data/lib/mkbrut/ops/base_op.rb +21 -0
- data/lib/mkbrut/ops/copy_file.rb +12 -0
- data/lib/mkbrut/ops/insert_code_in_method.rb +58 -0
- data/lib/mkbrut/ops/insert_route.rb +52 -0
- data/lib/mkbrut/ops/mkdir.rb +13 -0
- data/lib/mkbrut/ops/prism_parsing_op.rb +70 -0
- data/lib/mkbrut/ops/render_template.rb +26 -0
- data/lib/mkbrut/ops/skip_file.rb +10 -0
- data/lib/mkbrut/ops.rb +16 -0
- data/lib/mkbrut/organization.rb +5 -0
- data/lib/mkbrut/prefix.rb +26 -0
- data/lib/mkbrut/prefixed_io.rb +16 -0
- data/lib/mkbrut/segments/bare_bones.rb +184 -0
- data/lib/mkbrut/segments/demo.rb +117 -0
- data/lib/mkbrut/segments/sidekiq.rb +3 -0
- data/lib/mkbrut/segments.rb +7 -0
- data/lib/mkbrut/version.rb +3 -0
- data/lib/mkbrut/versions.rb +16 -0
- data/lib/mkbrut.rb +17 -0
- data/templates/Base/Dockerfile.dx +205 -0
- data/templates/Base/Gemfile.erb +53 -0
- data/templates/Base/Procfile.development +4 -0
- data/templates/Base/Procfile.test +1 -0
- data/templates/Base/README.md +4 -0
- data/templates/Base/README.md.erb +40 -0
- data/templates/Base/app/bootstrap.rb +61 -0
- data/templates/Base/app/config/i18n/en/1_defaults.rb +128 -0
- data/templates/Base/app/config/i18n/en/2_app.rb +24 -0
- data/templates/Base/app/public/static/manifest.json.erb +33 -0
- data/templates/Base/app/src/app.rb.erb +37 -0
- data/templates/Base/app/src/back_end/data_models/app_data_model.rb +5 -0
- data/templates/Base/app/src/back_end/data_models/db.rb +19 -0
- data/templates/Base/app/src/back_end/data_models/seed/seed_data.rb +9 -0
- data/templates/Base/app/src/front_end/components/app_component.rb +8 -0
- data/templates/Base/app/src/front_end/components/custom_element_registration.rb.erb +7 -0
- data/templates/Base/app/src/front_end/css/fonts.css +19 -0
- data/templates/Base/app/src/front_end/css/index.css +3 -0
- data/templates/Base/app/src/front_end/css/svgs.css +12 -0
- data/templates/Base/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
- data/templates/Base/app/src/front_end/forms/app_form.rb +4 -0
- data/templates/Base/app/src/front_end/handlers/app_handler.rb +4 -0
- data/templates/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
- data/templates/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
- data/templates/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
- data/templates/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
- data/templates/Base/app/src/front_end/images/favicon.ico +0 -0
- data/templates/Base/app/src/front_end/images/icon.png +0 -0
- data/templates/Base/app/src/front_end/images/mkicons.sh +6 -0
- data/templates/Base/app/src/front_end/js/index.js +6 -0
- data/templates/Base/app/src/front_end/layouts/default_layout.rb.erb +76 -0
- data/templates/Base/app/src/front_end/pages/app_page.rb +11 -0
- data/templates/Base/app/src/front_end/pages/home_page.rb.erb +54 -0
- data/templates/Base/app/src/front_end/support/app_session.rb +6 -0
- data/templates/Base/app/src/front_end/svgs/README.md +5 -0
- data/templates/Base/app/src/front_end/svgs/comment-button.svg +59 -0
- data/templates/Base/bin/README.md.erb +5 -0
- data/templates/Base/bin/build-assets +7 -0
- data/templates/Base/bin/ci +39 -0
- data/templates/Base/bin/console +31 -0
- data/templates/Base/bin/db +9 -0
- data/templates/Base/bin/dbconsole +51 -0
- data/templates/Base/bin/dev +25 -0
- data/templates/Base/bin/release +26 -0
- data/templates/Base/bin/run +86 -0
- data/templates/Base/bin/scaffold +9 -0
- data/templates/Base/bin/setup +256 -0
- data/templates/Base/bin/test +9 -0
- data/templates/Base/bin/test-server +29 -0
- data/templates/Base/bin/watch-and-build-assets +37 -0
- data/templates/Base/config.ru +16 -0
- data/templates/Base/docker-compose.dx.yml +85 -0
- data/templates/Base/dx/README.md +28 -0
- data/templates/Base/dx/bash_customizations +12 -0
- data/templates/Base/dx/bash_customizations.local +4 -0
- data/templates/Base/dx/build +101 -0
- data/templates/Base/dx/docker-compose.env.erb +25 -0
- data/templates/Base/dx/dx.sh.lib +137 -0
- data/templates/Base/dx/exec +56 -0
- data/templates/Base/dx/prune +19 -0
- data/templates/Base/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/templates/Base/dx/start +30 -0
- data/templates/Base/dx/stop +23 -0
- data/templates/Base/package.json.erb +37 -0
- data/templates/Base/puma.config.rb +53 -0
- data/templates/Base/specs/e2e/home_page.spec.rb.erb +23 -0
- data/templates/Base/specs/front_end/js/SpecHelper.js +24 -0
- data/templates/Base/specs/front_end/pages/home_page.spec.rb +22 -0
- data/templates/Base/specs/lint_factories.spec.rb +7 -0
- data/templates/Base/specs/spec_helper.rb +78 -0
- data/templates/Base/specs/support.rb +2 -0
- data/templates/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +24 -0
- data/templates/segments/BareBones/app/src/front_end/js/Example.js.erb +49 -0
- data/templates/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +41 -0
- data/templates/segments/BareBones/specs/front_end/js/Example.spec.js.erb +38 -0
- data/templates/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +3 -0
- data/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +15 -0
- data/templates/segments/Demo/app/src/front_end/components/flash_component.rb +36 -0
- data/templates/segments/Demo/app/src/front_end/css/constraint-violations.css +18 -0
- data/templates/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +4 -0
- data/templates/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +64 -0
- data/templates/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +41 -0
- data/templates/segments/Demo/app/src/front_end/pages/guestbook_page.rb +43 -0
- data/templates/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +64 -0
- data/templates/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +5 -0
- data/templates/segments/Demo/specs/e2e/guest_message.spec.rb +54 -0
- data/templates/segments/Demo/specs/factories/db/guestbook_message.rb +7 -0
- data/templates/segments/Demo/specs/front_end/components/flash_component.spec.rb +5 -0
- data/templates/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +122 -0
- data/templates/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +5 -0
- data/templates/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +52 -0
- data/templates/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +5 -0
- metadata +195 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
import { withHTML } from "./SpecHelper.js"
|
2
|
+
|
3
|
+
// Autonomouse custom element support is very basic.
|
4
|
+
// In this spec, we use `withHTML` which was defined in SpecHelper.js,
|
5
|
+
// which ultimately uses `createTestBasedOnHTML` from BrutJS.
|
6
|
+
// This will configure JSDom with the HTML you provide, then execute
|
7
|
+
// your test as if that HTML is the document.
|
8
|
+
describe("<<%= prefix %>-example>", () => {
|
9
|
+
|
10
|
+
// The example custom element will transform its contents to
|
11
|
+
// either upper or lower case. In this test, we assert that, by
|
12
|
+
// default, it transforms to upper case. To do that,
|
13
|
+
// create an HTML document you can see below. The element
|
14
|
+
// will have been connected (connectedCallback will have been
|
15
|
+
// called) by the time the test executes. Thus,
|
16
|
+
// document.querySelector(...) will find the element and its
|
17
|
+
// .textContent will have already been transformed.
|
18
|
+
withHTML(`
|
19
|
+
<<%= prefix %>-example>This is some Text</<%= prefix %>-example>
|
20
|
+
`).test("upper case by default", ({document,assert}) => {
|
21
|
+
const element = document.querySelector("<%= prefix %>-example")
|
22
|
+
assert.equal(element.textContent,"THIS IS SOME TEXT")
|
23
|
+
})
|
24
|
+
|
25
|
+
withHTML(`
|
26
|
+
<<%= prefix %>-example transform="lower">This is some Text</<%= prefix %>-example>
|
27
|
+
`).test("lower case when asked", ({document,assert}) => {
|
28
|
+
const element = document.querySelector("<%= prefix %>-example")
|
29
|
+
assert.equal(element.textContent,"this is some text")
|
30
|
+
// Remember that setAttribute is synchronous, so by the time
|
31
|
+
// its done executing, attributeChangedCallback will have been called,
|
32
|
+
// and thus the element's `update()` method will have been called and
|
33
|
+
// its textContent transformed.
|
34
|
+
element.setAttribute("transform","upper")
|
35
|
+
assert.equal(element.textContent,"THIS IS SOME TEXT")
|
36
|
+
})
|
37
|
+
})
|
38
|
+
|
data/templates/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
run %{CREATE EXTENSION IF NOT EXISTS citext}
|
4
|
+
create_table :guestbook_messages,
|
5
|
+
comment: "Messages people have left in the guestbook",
|
6
|
+
external_id: true do
|
7
|
+
|
8
|
+
column :name, :text
|
9
|
+
column :message, :text
|
10
|
+
column :ip_address, :inet
|
11
|
+
|
12
|
+
key [ :ip_address ]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class FlashComponent < AppComponent
|
2
|
+
def initialize(flash:)
|
3
|
+
@flash = flash
|
4
|
+
end
|
5
|
+
|
6
|
+
CLASSES = [
|
7
|
+
"f-1",
|
8
|
+
"fw-bold",
|
9
|
+
"w-50",
|
10
|
+
"mv-3",
|
11
|
+
"shadow-3",
|
12
|
+
"pa-3",
|
13
|
+
"ba",
|
14
|
+
"br-3",
|
15
|
+
"tc"
|
16
|
+
]
|
17
|
+
COLORS = [
|
18
|
+
"%{color}-300",
|
19
|
+
"bg-%{color}-800",
|
20
|
+
"bc-%{color}-700",
|
21
|
+
]
|
22
|
+
|
23
|
+
def view_template
|
24
|
+
if @flash.notice?
|
25
|
+
div(role: "status",
|
26
|
+
class: CLASSES + COLORS.map { it % { color: "blue" } }) do
|
27
|
+
t(@flash.notice)
|
28
|
+
end
|
29
|
+
elsif @flash.alert?
|
30
|
+
div(role: "alert",
|
31
|
+
class: CLASSES + COLORS.map { it % { color: "orange" } }) do
|
32
|
+
t(@flash.alert)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
/* Hide <brut-cv> tags so that we don't see
|
2
|
+
* client-side errors before the form is submitted */
|
3
|
+
brut-cv {
|
4
|
+
display: none;
|
5
|
+
color: var(--red-400);
|
6
|
+
}
|
7
|
+
|
8
|
+
/* Server-side errors are always visible */
|
9
|
+
brut-cv[server-side] {
|
10
|
+
display: block;
|
11
|
+
}
|
12
|
+
|
13
|
+
/* Only when a form submission is attempted do
|
14
|
+
* we show the <brut-cv>s that are not server-side,
|
15
|
+
* and thus client-side */
|
16
|
+
brut-form[submitted-invalid] brut-cv {
|
17
|
+
display: block;
|
18
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class GuestbookMessageHandler < AppHandler
|
2
|
+
def initialize(form:, flash:, rack_request_ip:, session:)
|
3
|
+
@form = form
|
4
|
+
@flash = flash
|
5
|
+
@rack_request_ip = rack_request_ip
|
6
|
+
@session = session
|
7
|
+
end
|
8
|
+
def handle
|
9
|
+
# If client-side constraint violation checking was skipped,
|
10
|
+
# but there ARE such violations, the form will be able to check
|
11
|
+
# that server-side and return true for #constraint_violations?
|
12
|
+
if !@form.constraint_violations?
|
13
|
+
save_message
|
14
|
+
end
|
15
|
+
|
16
|
+
if @form.constraint_violations?
|
17
|
+
@flash.alert = :guestbook_not_saved
|
18
|
+
return NewGuestbookMessagePage.new(form: @form,
|
19
|
+
session: @session)
|
20
|
+
else
|
21
|
+
@flash.notice = :guestbook_saved
|
22
|
+
redirect_to(
|
23
|
+
GuestbookPage,
|
24
|
+
anchor: @session.guestbook_message&.external_id
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def save_message
|
32
|
+
# While we don't recommend business logic be placed
|
33
|
+
# inside handlers, we're leaving it here to avoid
|
34
|
+
# anchoring you on any particular way of managing
|
35
|
+
# the logic in your app.
|
36
|
+
if @form.message.split(/\s+/).length < 2
|
37
|
+
@form.server_side_constraint_violation(
|
38
|
+
input_name: "message",
|
39
|
+
key: :not_enough_words
|
40
|
+
)
|
41
|
+
else
|
42
|
+
if DB::GuestbookMessage.find(ip_address: @rack_request_ip)
|
43
|
+
@form.server_side_constraint_violation(
|
44
|
+
input_name: "name",
|
45
|
+
key: :already_posted
|
46
|
+
)
|
47
|
+
else
|
48
|
+
begin
|
49
|
+
guestbook_message = DB::GuestbookMessage.create(
|
50
|
+
name: @form.name,
|
51
|
+
message: @form.message,
|
52
|
+
ip_address: @rack_request_ip,
|
53
|
+
)
|
54
|
+
@session.signed_guestbook(guestbook_message)
|
55
|
+
rescue Sequel::UniqueConstraintViolation => ex
|
56
|
+
@form.server_side_constraint_violation(
|
57
|
+
input_name: "name",
|
58
|
+
key: :already_posted
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class GuestbookPage::MessageComponent < AppComponent
|
2
|
+
def initialize(message:, index:, current_visitor_message: false)
|
3
|
+
@message = message
|
4
|
+
@current_visitor_message = current_visitor_message
|
5
|
+
@background_color = [
|
6
|
+
"bg-gray-800",
|
7
|
+
"bg-blue-800",
|
8
|
+
"bg-green-800",
|
9
|
+
"bg-yellow-800",
|
10
|
+
][index % 4]
|
11
|
+
end
|
12
|
+
|
13
|
+
def view_template
|
14
|
+
div_classes = [
|
15
|
+
"measure",
|
16
|
+
"mh-auto",
|
17
|
+
"pa-3",
|
18
|
+
"br-4",
|
19
|
+
"br-tr-0",
|
20
|
+
"br-bl-0",
|
21
|
+
"mt-3",
|
22
|
+
@background_color,
|
23
|
+
]
|
24
|
+
border_class = if @current_visitor_message
|
25
|
+
div_classes << "ba" << "bc-gray-600" << "bw-2"
|
26
|
+
else
|
27
|
+
div_classes << "bn"
|
28
|
+
end
|
29
|
+
div(class: div_classes, id: @message.external_id) do
|
30
|
+
blockquote(class: "ff-cursive p mh-auto i f-4") do
|
31
|
+
@message.message
|
32
|
+
end
|
33
|
+
p(class: "p mh-auto tr f-2") do
|
34
|
+
plain("—")
|
35
|
+
plain(@message.name)
|
36
|
+
plain(" on ")
|
37
|
+
TimeTag(date: @message.created_at, format: :date, class: "gray-400 f-1 ttu")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class GuestbookPage < AppPage
|
2
|
+
|
3
|
+
def initialize(session:)
|
4
|
+
@session = session
|
5
|
+
end
|
6
|
+
|
7
|
+
def page_template
|
8
|
+
div(class: "flex flex-column items-center ff-sans") do
|
9
|
+
article(class: "w-50 pv-3 bt bc-gray-700", id: "guestbook") do
|
10
|
+
h2(class: "ff-sans ma-0 lh-title f-4 tc") do
|
11
|
+
"Check out the Guestbook!"
|
12
|
+
end
|
13
|
+
if !@session.signed_guestbook?
|
14
|
+
a(
|
15
|
+
href: NewGuestbookMessagePage.routing,
|
16
|
+
class: "db tc mv-3 f-2 fw-5 blue-300"
|
17
|
+
) do
|
18
|
+
"Sign it, yourself!"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
a(
|
22
|
+
href: "##{@session.guestbook_message.external_id}",
|
23
|
+
class: "db tc mv-3 f-2 fw-5 blue-300"
|
24
|
+
) do
|
25
|
+
"Check out your message"
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
DB::GuestbookMessage.order(
|
30
|
+
Sequel.function(:random)
|
31
|
+
).each_with_index do |message,index|
|
32
|
+
render(
|
33
|
+
GuestbookPage::MessageComponent.new(
|
34
|
+
message:,
|
35
|
+
index:,
|
36
|
+
current_visitor_message: message == @session.guestbook_message
|
37
|
+
)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class NewGuestbookMessagePage < AppPage
|
2
|
+
def initialize(form: nil, session:)
|
3
|
+
@form = form || GuestbookMessageForm.new
|
4
|
+
@session = session
|
5
|
+
end
|
6
|
+
|
7
|
+
def before_generate
|
8
|
+
if @session.signed_guestbook?
|
9
|
+
redirect_to(GuestbookPage)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def page_template
|
14
|
+
div(class: "flex flex-column items-center ff-sans") do
|
15
|
+
render global_component(FlashComponent)
|
16
|
+
div(class: "w-50 mv-5") do
|
17
|
+
brut_form(class: "w-100") do
|
18
|
+
FormTag(for: @form, class: "flex flex-column gap-3 items-end") do
|
19
|
+
h1(class: "ff-sans ma-0 lh-title f-5 self-start flex items-center gap-3") do
|
20
|
+
img(src: "/static/images/icon.png", class: "w-5")
|
21
|
+
span { "Sign The Guestbook" }
|
22
|
+
end
|
23
|
+
label(class: "flex flex-column gap-1 w-100") do
|
24
|
+
Inputs::InputTag(
|
25
|
+
form: @form,
|
26
|
+
input_name: "name",
|
27
|
+
placeholder: "e.g. Pat",
|
28
|
+
class: "f-3 pa-2 inset-shadow-1 br-2",
|
29
|
+
"data-1p-ignore": true
|
30
|
+
)
|
31
|
+
div(class: "flex items-start gap-3") do
|
32
|
+
span { "Your Message" }
|
33
|
+
ConstraintViolations(
|
34
|
+
form: @form,
|
35
|
+
input_name: "name"
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
label(class: "flex flex-column gap-1 w-100") do
|
41
|
+
Inputs::TextareaTag(
|
42
|
+
form: @form,
|
43
|
+
input_name: "message",
|
44
|
+
placeholder: "e.g. What a great site!",
|
45
|
+
rows: 6,
|
46
|
+
class: "f-3 pa-2 inset-shadow-1 br-2"
|
47
|
+
)
|
48
|
+
div(class: "flex items-start gap-3") do
|
49
|
+
span { "Your Message" }
|
50
|
+
ConstraintViolations(
|
51
|
+
form: @form,
|
52
|
+
input_name: "message"
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
button(class: "bg-green-900 green-300 ph-5 pv-3 f-3 ba bc-green-600 br-pill shadow-1 pointer") do
|
57
|
+
"Sign!"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "You can leave a guestbook message" do
|
4
|
+
it "accepts your message after validation, then shows it with the others" do
|
5
|
+
page.goto(HomePage.routing)
|
6
|
+
|
7
|
+
link = page.locator("a[href='#{NewGuestbookMessagePage.routing}']")
|
8
|
+
|
9
|
+
link.click
|
10
|
+
|
11
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
12
|
+
|
13
|
+
button = page.locator("form button")
|
14
|
+
|
15
|
+
button.click
|
16
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
17
|
+
|
18
|
+
name_error_message = page.locator("brut-cv-messages[input-name='name']")
|
19
|
+
message_error_message = page.locator("brut-cv-messages[input-name='message']")
|
20
|
+
|
21
|
+
this_field = t("cv.this_field")
|
22
|
+
|
23
|
+
expect(name_error_message).to have_text(
|
24
|
+
t("cv.fe.valueMissing", field: this_field)
|
25
|
+
)
|
26
|
+
expect(message_error_message).to have_text(
|
27
|
+
t("cv.fe.valueMissing", field: this_field)
|
28
|
+
)
|
29
|
+
|
30
|
+
name_field = page.locator("form input[name='name']")
|
31
|
+
message_field = page.locator("form textarea[name='message']")
|
32
|
+
|
33
|
+
name_field.fill("Pat")
|
34
|
+
message_field.fill("FIRSTPOST!")
|
35
|
+
|
36
|
+
button.click
|
37
|
+
expect(page).to be_page_for(NewGuestbookMessagePage)
|
38
|
+
|
39
|
+
flash = page.locator("[role='alert']")
|
40
|
+
expect(flash).to have_text(t(:guestbook_not_saved))
|
41
|
+
expect(message_error_message).to have_text(
|
42
|
+
t("cv.be.not_enough_words", field: this_field)
|
43
|
+
)
|
44
|
+
|
45
|
+
message_field.fill("OK, fine, this site is awesome!")
|
46
|
+
button.click
|
47
|
+
|
48
|
+
expect(page).to be_page_for(GuestbookPage)
|
49
|
+
|
50
|
+
quotes = page.locator("blockquote")
|
51
|
+
expect(quotes).to have_text("OK, fine, this site is awesome!")
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe GuestbookMessageHandler do
|
4
|
+
describe "#handle!" do
|
5
|
+
context "there are client-side constraint violations" do
|
6
|
+
it "generates the new guestbook message page" do
|
7
|
+
form = GuestbookMessageForm.new(params: {})
|
8
|
+
flash = empty_flash
|
9
|
+
session = empty_session
|
10
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
11
|
+
|
12
|
+
confidence_check do
|
13
|
+
expect(form).to have_constraint_violation(:name,key: :value_missing)
|
14
|
+
end
|
15
|
+
|
16
|
+
handler = described_class.new(
|
17
|
+
form:,
|
18
|
+
flash:,
|
19
|
+
session:,
|
20
|
+
rack_request_ip:
|
21
|
+
)
|
22
|
+
result = nil
|
23
|
+
expect {
|
24
|
+
result = handler.handle
|
25
|
+
}.not_to change { DB::GuestbookMessage.count }
|
26
|
+
|
27
|
+
expect(result).to have_rendered(NewGuestbookMessagePage)
|
28
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
context "there are server-side constraint violations" do
|
32
|
+
it "indicates a server-side error and generates the new guestbook message page" do
|
33
|
+
form = GuestbookMessageForm.new(params: {
|
34
|
+
name: "Pat",
|
35
|
+
message: "FIRSTPOST!!!!",
|
36
|
+
})
|
37
|
+
flash = empty_flash
|
38
|
+
session = empty_session
|
39
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
40
|
+
|
41
|
+
handler = described_class.new(
|
42
|
+
form:,
|
43
|
+
flash:,
|
44
|
+
session:,
|
45
|
+
rack_request_ip:
|
46
|
+
)
|
47
|
+
result = nil
|
48
|
+
expect {
|
49
|
+
result = handler.handle
|
50
|
+
}.not_to change { DB::GuestbookMessage.count }
|
51
|
+
|
52
|
+
expect(result).to have_rendered(NewGuestbookMessagePage)
|
53
|
+
expect(form).to have_constraint_violation(:message, key: :not_enough_words)
|
54
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
context "the ip address has already been used" do
|
58
|
+
|
59
|
+
it "indicates a server-side error and generates the new guestbook message page" do
|
60
|
+
existing_message = create(:guestbook_message)
|
61
|
+
|
62
|
+
form = GuestbookMessageForm.new(params: {
|
63
|
+
name: "Pat",
|
64
|
+
message: "This website is amazing!",
|
65
|
+
})
|
66
|
+
flash = empty_flash
|
67
|
+
session = empty_session
|
68
|
+
rack_request_ip = existing_message.ip_address
|
69
|
+
|
70
|
+
handler = described_class.new(
|
71
|
+
form:,
|
72
|
+
flash:,
|
73
|
+
session:,
|
74
|
+
rack_request_ip:
|
75
|
+
)
|
76
|
+
result = nil
|
77
|
+
expect {
|
78
|
+
result = handler.handle
|
79
|
+
}.not_to change { DB::GuestbookMessage.count }
|
80
|
+
|
81
|
+
expect(result).to have_rendered(NewGuestbookMessagePage)
|
82
|
+
expect(form).to have_constraint_violation(:name, key: :already_posted)
|
83
|
+
expect(flash.alert).to eq("guestbook_not_saved")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
context "no constraint violations, unused ip address" do
|
87
|
+
it "saves the message and redirects back to the guestbook page" do
|
88
|
+
form = GuestbookMessageForm.new(params: {
|
89
|
+
name: "Pat",
|
90
|
+
message: "This website is amazing!",
|
91
|
+
})
|
92
|
+
flash = empty_flash
|
93
|
+
session = empty_session
|
94
|
+
rack_request_ip = Faker::Internet.ip_v4_address
|
95
|
+
|
96
|
+
handler = described_class.new(
|
97
|
+
form:,
|
98
|
+
flash:,
|
99
|
+
session:,
|
100
|
+
rack_request_ip:
|
101
|
+
)
|
102
|
+
result = nil
|
103
|
+
expect {
|
104
|
+
result = handler.handle
|
105
|
+
}.to change { DB::GuestbookMessage.count }.by(1)
|
106
|
+
|
107
|
+
message = DB::GuestbookMessage.order(Sequel.desc(:created_at)).first
|
108
|
+
|
109
|
+
expect(message.name).to eq("Pat")
|
110
|
+
expect(message.message).to eq("This website is amazing!")
|
111
|
+
expect(message.ip_address).to eq(rack_request_ip)
|
112
|
+
|
113
|
+
expect(result).to have_redirected_to(GuestbookPage,
|
114
|
+
anchor: message.external_id)
|
115
|
+
expect(form).not_to have_constraint_violation(:name,
|
116
|
+
key: :already_posted)
|
117
|
+
expect(flash.notice).to eq("guestbook_saved")
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe GuestbookPage do
|
4
|
+
it "shows current guestbook messages" do
|
5
|
+
messages = 5.times.map {
|
6
|
+
create(:guestbook_message)
|
7
|
+
}
|
8
|
+
confidence_check { expect(DB::GuestbookMessage.all.length).to eq(5) }
|
9
|
+
|
10
|
+
result = generate_and_parse(self.described_class.new(session: empty_session))
|
11
|
+
|
12
|
+
message_elements = result.css("div:has(> blockquote)")
|
13
|
+
expect(message_elements.length).to eq(5)
|
14
|
+
messages.each do |message|
|
15
|
+
element = message_elements.select {
|
16
|
+
it.css("blockquote").text.strip == message.message
|
17
|
+
}
|
18
|
+
expect(element.size).to eq(1),
|
19
|
+
"Found #{element.size} blockquotes for message #{message.external_id}, instead of exactly 1"
|
20
|
+
expect(element[0].css("p").text).to include(message.name)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
context "when you've signed the guestbook" do
|
24
|
+
it "shows a link to view your message and not a link to make one" do
|
25
|
+
message = create(:guestbook_message)
|
26
|
+
session = empty_session
|
27
|
+
|
28
|
+
session.signed_guestbook(message)
|
29
|
+
|
30
|
+
result = generate_and_parse(self.described_class.new(session:))
|
31
|
+
|
32
|
+
add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
|
33
|
+
expect(add_link).to eq(nil)
|
34
|
+
|
35
|
+
view_link = result.e("a[href='##{message.external_id}']")
|
36
|
+
expect(view_link).not_to eq(nil)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
context "when you have not signed the guestbook" do
|
40
|
+
it "shows a link to view your message and not a link to make one" do
|
41
|
+
message = create(:guestbook_message)
|
42
|
+
|
43
|
+
result = generate_and_parse(self.described_class.new(session: empty_session))
|
44
|
+
|
45
|
+
add_link = result.e("a[href='#{NewGuestbookMessagePage.routing}']")
|
46
|
+
expect(add_link).not_to eq(nil)
|
47
|
+
|
48
|
+
view_link = result.e("a[href='##{message.external_id}']")
|
49
|
+
expect(view_link).to eq(nil)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|