brut 0.17.0 → 0.18.1

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 (1103) hide show
  1. checksums.yaml +4 -4
  2. data/exe/brut +34 -0
  3. data/lib/brut/cli/apps/build_assets.rb +78 -48
  4. data/lib/brut/cli/apps/db.rb +168 -202
  5. data/lib/brut/cli/apps/deploy.rb +291 -0
  6. data/lib/brut/cli/apps/heroku_container_based_deploy.rb +6 -0
  7. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/add_segment.rb +5 -5
  8. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/add_segment_options.rb +1 -1
  9. data/lib/brut/cli/apps/new/app.rb +240 -0
  10. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/app_id.rb +1 -1
  11. data/lib/brut/cli/apps/new/app_name.rb +29 -0
  12. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/base.rb +9 -6
  13. data/lib/brut/cli/apps/new/erb_binding_delegate.rb +23 -0
  14. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/internet_identifier.rb +5 -5
  15. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/invalid_identifier.rb +1 -1
  16. data/{mkbrut/lib/mkbrut/app.rb → lib/brut/cli/apps/new/old_app.rb} +8 -11
  17. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_css_import.rb +1 -1
  18. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_i18n_message.rb +1 -1
  19. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_method.rb +1 -1
  20. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/append_to_file.rb +1 -1
  21. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/base_op.rb +3 -3
  22. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/copy_file.rb +1 -1
  23. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_code_in_method.rb +1 -1
  24. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_into_file.rb +1 -1
  25. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_route.rb +1 -1
  26. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/mkdir.rb +1 -1
  27. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/prism_parsing_op.rb +1 -1
  28. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/render_template.rb +1 -1
  29. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/skip_file.rb +1 -1
  30. data/lib/brut/cli/apps/new/ops.rb +17 -0
  31. data/lib/brut/cli/apps/new/organization.rb +5 -0
  32. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/prefix.rb +1 -1
  33. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/bare_bones.rb +12 -11
  34. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/demo.rb +16 -15
  35. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/heroku.rb +9 -5
  36. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/sidekiq.rb +44 -21
  37. data/lib/brut/cli/apps/new/segments.rb +8 -0
  38. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/versions.rb +3 -3
  39. data/lib/brut/cli/apps/new.rb +26 -0
  40. data/lib/brut/cli/apps/scaffold.rb +150 -141
  41. data/lib/brut/cli/apps/test.rb +92 -68
  42. data/lib/brut/cli/commands/base_command.rb +174 -0
  43. data/lib/brut/cli/commands/compound_command.rb +29 -0
  44. data/lib/brut/cli/commands/execution_context.rb +32 -0
  45. data/lib/brut/cli/commands/help.rb +26 -0
  46. data/lib/brut/cli/commands/output_error.rb +13 -0
  47. data/lib/brut/cli/commands/raise_error.rb +11 -0
  48. data/lib/brut/cli/commands.rb +8 -0
  49. data/lib/brut/cli/execute_result.rb +39 -0
  50. data/lib/brut/cli/executor.rb +9 -4
  51. data/lib/brut/cli/output.rb +13 -0
  52. data/lib/brut/cli/parsed_command_line.rb +143 -0
  53. data/lib/brut/cli/runner.rb +124 -0
  54. data/lib/brut/cli.rb +7 -29
  55. data/lib/brut/framework/container.rb +1 -1
  56. data/lib/brut/framework/mcp.rb +59 -13
  57. data/lib/brut/framework/project_environment.rb +3 -1
  58. data/lib/brut/junk_drawer.rb +3 -1
  59. data/lib/brut/spec_support/cli_command_support.rb +45 -0
  60. data/lib/brut/spec_support/e2e_test_server.rb +3 -0
  61. data/lib/brut/spec_support/general_support.rb +1 -1
  62. data/lib/brut/spec_support/matchers/have_executed.rb +35 -0
  63. data/lib/brut/spec_support/rspec_setup.rb +4 -8
  64. data/lib/brut/spec_support.rb +1 -0
  65. data/lib/brut/tui/markup_string.rb +2 -0
  66. data/lib/brut/tui/script/events/command_std_out.rb +3 -2
  67. data/lib/brut/tui/script/exec_step.rb +11 -4
  68. data/lib/brut/tui/script/puts_subscriber.rb +4 -4
  69. data/lib/brut/tui/script.rb +7 -3
  70. data/lib/brut/tui/terminal_theme.rb +15 -11
  71. data/lib/brut/version.rb +1 -1
  72. data/templates/Base/.env.development.local +2 -0
  73. data/templates/Base/bin/ci +42 -0
  74. data/{mkbrut/templates → templates}/Base/bin/release +2 -2
  75. data/templates/Base/bin/setup +174 -0
  76. data/{mkbrut/templates → templates}/Base/bin/watch-and-build-assets +1 -1
  77. data/{mkbrut/templates → templates}/Base/dx/docker-compose.env.erb +1 -1
  78. data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/css/fonts.css +1 -1
  79. data/{mkbrut/templates → templates}/segments/Heroku/deploy/Dockerfile +2 -2
  80. data/templates/segments/Heroku/deploy/docker_config.rb +30 -0
  81. metadata +190 -1055
  82. data/.gitignore +0 -61
  83. data/.projections.json +0 -10
  84. data/CHANGELOG.md +0 -172
  85. data/CODE_OF_CONDUCT.txt +0 -99
  86. data/Dockerfile.dx +0 -82
  87. data/Gemfile +0 -6
  88. data/Gemfile.lock +0 -246
  89. data/LICENSE.txt +0 -370
  90. data/README.md +0 -90
  91. data/Rakefile +0 -25
  92. data/assets/Logo-Square.pxd +0 -0
  93. data/assets/LogoPylon.pxd +0 -0
  94. data/assets/LogoStop.pxd +0 -0
  95. data/assets/LogoTall.pxd +0 -0
  96. data/assets/MetroIcon.graffle +0 -0
  97. data/assets/MetroLogo.graffle +0 -0
  98. data/assets/SocialImage.png +0 -0
  99. data/assets/SocialImage.pxd +0 -0
  100. data/assets/YouTubeThumb.pxd +0 -0
  101. data/bin/bin_kit.rb +0 -51
  102. data/bin/build +0 -86
  103. data/bin/ci +0 -40
  104. data/bin/dev +0 -20
  105. data/bin/docs +0 -86
  106. data/bin/generate-and-run-rubocop +0 -52
  107. data/bin/new-version +0 -8
  108. data/bin/publish +0 -61
  109. data/bin/rake +0 -27
  110. data/bin/rspec +0 -27
  111. data/bin/rubocop +0 -27
  112. data/bin/setup +0 -252
  113. data/bin/test +0 -18
  114. data/brut-css/.nvim.lua +0 -1
  115. data/brut-css/README.md +0 -28
  116. data/brut-css/bin/build +0 -50
  117. data/brut-css/bin/ci +0 -19
  118. data/brut-css/bin/dev +0 -1
  119. data/brut-css/bin/docs +0 -34
  120. data/brut-css/bin/publish +0 -21
  121. data/brut-css/bin/setup +0 -6
  122. data/brut-css/config/media-queries-all.css +0 -15
  123. data/brut-css/config/media-queries-minimal.css +0 -5
  124. data/brut-css/config/postcss.config.cjs +0 -7
  125. data/brut-css/config/pseudo-classes-all.css +0 -9
  126. data/brut-css/dx +0 -1
  127. data/brut-css/package-lock.json +0 -3165
  128. data/brut-css/package.json +0 -36
  129. data/brut-css/src/css/appearance.css +0 -145
  130. data/brut-css/src/css/border.css +0 -522
  131. data/brut-css/src/css/colors.css +0 -3502
  132. data/brut-css/src/css/dimensions.css +0 -548
  133. data/brut-css/src/css/flex.css +0 -179
  134. data/brut-css/src/css/index.css +0 -13
  135. data/brut-css/src/css/layout.css +0 -120
  136. data/brut-css/src/css/list.css +0 -41
  137. data/brut-css/src/css/positioning.css +0 -354
  138. data/brut-css/src/css/properties/colors.css +0 -455
  139. data/brut-css/src/css/properties/index.css +0 -3
  140. data/brut-css/src/css/properties/spacing.css +0 -140
  141. data/brut-css/src/css/properties/typography.css +0 -224
  142. data/brut-css/src/css/reset.css +0 -107
  143. data/brut-css/src/css/spacing.css +0 -585
  144. data/brut-css/src/css/typography.css +0 -519
  145. data/brut-css/src/css/utils.css +0 -104
  146. data/brut-css/src/docs/1_getting-started/1_overview.md +0 -46
  147. data/brut-css/src/docs/1_getting-started/2_installation.md +0 -25
  148. data/brut-css/src/docs/1_getting-started/3_core-concepts.md +0 -75
  149. data/brut-css/src/docs/1_getting-started/4_simple-example.md +0 -132
  150. data/brut-css/src/docs/1_getting-started/page.html.ejs +0 -10
  151. data/brut-css/src/docs/2_properties/page.html.ejs +0 -71
  152. data/brut-css/src/docs/3_classes/color-demo.html.ejs +0 -31
  153. data/brut-css/src/docs/3_classes/page.html.ejs +0 -87
  154. data/brut-css/src/docs/4_customization/1_design-system.md +0 -36
  155. data/brut-css/src/docs/4_customization/2_breakpoints.md +0 -75
  156. data/brut-css/src/docs/4_customization/3_pseudo-classes.md +0 -74
  157. data/brut-css/src/docs/4_customization/4_advanced-configuration.md +0 -40
  158. data/brut-css/src/docs/4_customization/page.html.ejs +0 -10
  159. data/brut-css/src/docs/docs.css +0 -98
  160. data/brut-css/src/docs/includes/body-and-header.html.ejs +0 -30
  161. data/brut-css/src/docs/includes/footer-and-rest.html.ejs +0 -9
  162. data/brut-css/src/docs/includes/head.html.ejs +0 -5
  163. data/brut-css/src/docs/includes/nav.html.ejs +0 -10
  164. data/brut-css/src/docs/index.html.ejs +0 -32
  165. data/brut-css/src/docs/prism-twilight.min.css +0 -1
  166. data/brut-css/src/js/Logger.js +0 -71
  167. data/brut-css/src/js/build.js +0 -111
  168. data/brut-css/src/js/cli/CLIArgError.js +0 -7
  169. data/brut-css/src/js/cli/Debug.js +0 -27
  170. data/brut-css/src/js/cli/DocsDir.js +0 -16
  171. data/brut-css/src/js/cli/DocsTemplateSourceDir.js +0 -16
  172. data/brut-css/src/js/cli/InputFile.js +0 -31
  173. data/brut-css/src/js/cli/MediaQueryConfigFile.js +0 -10
  174. data/brut-css/src/js/cli/OutputFile.js +0 -22
  175. data/brut-css/src/js/cli/ParsedArg.js +0 -17
  176. data/brut-css/src/js/cli/PathToBrutCSSRoot.js +0 -19
  177. data/brut-css/src/js/cli/PseudoClassConfigFile.js +0 -11
  178. data/brut-css/src/js/cli.js +0 -108
  179. data/brut-css/src/js/docGenerator.js +0 -467
  180. data/brut-css/src/js/mediaQueryConfigParser.js +0 -98
  181. data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +0 -49
  182. data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +0 -42
  183. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +0 -9
  184. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +0 -185
  185. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +0 -8
  186. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +0 -7
  187. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +0 -73
  188. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +0 -9
  189. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +0 -4
  190. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +0 -8
  191. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +0 -12
  192. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +0 -4
  193. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +0 -8
  194. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +0 -5
  195. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +0 -9
  196. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +0 -49
  197. data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +0 -45
  198. data/brut-css/src/js/pseudoClassConfigParser.js +0 -145
  199. data/brut-js/.projections.json +0 -10
  200. data/brut-js/README.md +0 -118
  201. data/brut-js/bin/build +0 -19
  202. data/brut-js/bin/ci +0 -5
  203. data/brut-js/bin/docs +0 -25
  204. data/brut-js/bin/publish +0 -21
  205. data/brut-js/bin/setup +0 -6
  206. data/brut-js/docs/README.md +0 -8
  207. data/brut-js/docs/jsdoc-plugins/customElementTag.js +0 -8
  208. data/brut-js/docs/jsdoc-theme/publish.js +0 -692
  209. data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +0 -25
  210. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +0 -202
  211. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +0 -2
  212. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +0 -28
  213. data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +0 -327
  214. data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +0 -111
  215. data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +0 -132
  216. data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +0 -10
  217. data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +0 -199
  218. data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +0 -143
  219. data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +0 -2
  220. data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +0 -13
  221. data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +0 -32
  222. data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +0 -38
  223. data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +0 -14
  224. data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +0 -38
  225. data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +0 -131
  226. data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +0 -14
  227. data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +0 -131
  228. data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +0 -108
  229. data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +0 -19
  230. data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +0 -8
  231. data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +0 -19
  232. data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +0 -7
  233. data/brut-js/docs/jsdoc.config.json +0 -23
  234. data/brut-js/docs/package-lock.json +0 -343
  235. data/brut-js/docs/package.json +0 -7
  236. data/brut-js/dx +0 -1
  237. data/brut-js/package-lock.json +0 -2210
  238. data/brut-js/package.json +0 -36
  239. data/brut-js/specs/AjaxSubmit.spec.js +0 -453
  240. data/brut-js/specs/Autosubmit.spec.js +0 -127
  241. data/brut-js/specs/ConfirmSubmit.spec.js +0 -224
  242. data/brut-js/specs/ConstraintViolationMessage.spec.js +0 -33
  243. data/brut-js/specs/ConstraintViolationMessages.spec.js +0 -32
  244. data/brut-js/specs/CopyToClipboard.spec.js +0 -35
  245. data/brut-js/specs/Form.spec.js +0 -137
  246. data/brut-js/specs/I18nTranslation.spec.js +0 -19
  247. data/brut-js/specs/LocaleDetection.spec.js +0 -22
  248. data/brut-js/specs/Message.spec.js +0 -15
  249. data/brut-js/specs/SpecHelper.js +0 -23
  250. data/brut-js/specs/Tabs.spec.js +0 -41
  251. data/brut-js/specs/Toast.spec.js +0 -34
  252. data/brut-js/specs/config/asset_metadata.json +0 -7
  253. data/brut-js/src/AjaxSubmit.js +0 -499
  254. data/brut-js/src/Autosubmit.js +0 -63
  255. data/brut-js/src/BaseCustomElement.js +0 -261
  256. data/brut-js/src/ConfirmSubmit.js +0 -137
  257. data/brut-js/src/ConfirmationDialog.js +0 -143
  258. data/brut-js/src/ConstraintViolationMessage.js +0 -140
  259. data/brut-js/src/ConstraintViolationMessages.js +0 -98
  260. data/brut-js/src/CopyToClipboard.js +0 -96
  261. data/brut-js/src/Form.js +0 -147
  262. data/brut-js/src/I18nTranslation.js +0 -64
  263. data/brut-js/src/LocaleDetection.js +0 -117
  264. data/brut-js/src/Logger.js +0 -90
  265. data/brut-js/src/Message.js +0 -62
  266. data/brut-js/src/RichString.js +0 -116
  267. data/brut-js/src/Tabs.js +0 -168
  268. data/brut-js/src/Toast.js +0 -102
  269. data/brut-js/src/Tracing.js +0 -247
  270. data/brut-js/src/appForTestingOnly.js +0 -15
  271. data/brut-js/src/index.js +0 -133
  272. data/brut-js/src/testing/AssetMetadata.js +0 -35
  273. data/brut-js/src/testing/AssetMetadataLoader.js +0 -25
  274. data/brut-js/src/testing/CustomElementTest.js +0 -235
  275. data/brut-js/src/testing/DOMCreator.js +0 -45
  276. data/brut-js/src/testing/index.js +0 -48
  277. data/brut.gemspec +0 -73
  278. data/brutrb.com/.vitepress/config.mjs +0 -164
  279. data/brutrb.com/.vitepress/plugins/jsdocLinker.js +0 -34
  280. data/brutrb.com/.vitepress/plugins/rdocLinker.js +0 -18
  281. data/brutrb.com/.vitepress/theme/custom.css +0 -14
  282. data/brutrb.com/.vitepress/theme/index.js +0 -18
  283. data/brutrb.com/.vitepress/theme/style.css +0 -139
  284. data/brutrb.com/adrs.md +0 -16
  285. data/brutrb.com/ai.md +0 -68
  286. data/brutrb.com/assets.md +0 -131
  287. data/brutrb.com/bin/build +0 -5
  288. data/brutrb.com/bin/deploy +0 -7
  289. data/brutrb.com/bin/dev +0 -5
  290. data/brutrb.com/bin/setup +0 -6
  291. data/brutrb.com/brut-js.md +0 -128
  292. data/brutrb.com/business-logic.md +0 -55
  293. data/brutrb.com/cli.md +0 -274
  294. data/brutrb.com/components.md +0 -265
  295. data/brutrb.com/configuration.md +0 -256
  296. data/brutrb.com/css.md +0 -103
  297. data/brutrb.com/custom-element-tests.md +0 -148
  298. data/brutrb.com/database-access.md +0 -201
  299. data/brutrb.com/database-schema.md +0 -320
  300. data/brutrb.com/deployment.md +0 -158
  301. data/brutrb.com/dev-environment.md +0 -186
  302. data/brutrb.com/dir-structure.md +0 -120
  303. data/brutrb.com/doc-conventions.md +0 -41
  304. data/brutrb.com/dx +0 -1
  305. data/brutrb.com/end-to-end-tests.md +0 -176
  306. data/brutrb.com/features.md +0 -373
  307. data/brutrb.com/flash-and-session.md +0 -208
  308. data/brutrb.com/form-constraints.md +0 -266
  309. data/brutrb.com/forms.md +0 -238
  310. data/brutrb.com/getting-started.md +0 -142
  311. data/brutrb.com/handlers.md +0 -177
  312. data/brutrb.com/hooks.md +0 -176
  313. data/brutrb.com/i18n.md +0 -190
  314. data/brutrb.com/images/DevEnvironment.graffle +0 -0
  315. data/brutrb.com/images/DevEnvironment.png +0 -0
  316. data/brutrb.com/images/LogoSquare.png +0 -0
  317. data/brutrb.com/images/LogoStop.png +0 -0
  318. data/brutrb.com/images/LogoTall.png +0 -0
  319. data/brutrb.com/images/Makefile +0 -10
  320. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  321. data/brutrb.com/images/OverviewMetro.png +0 -0
  322. data/brutrb.com/images/dev-env-overview.dot +0 -54
  323. data/brutrb.com/images/dev-env-overview.png +0 -0
  324. data/brutrb.com/images/dev-env-protocol.dot +0 -37
  325. data/brutrb.com/images/dev-env-protocol.png +0 -0
  326. data/brutrb.com/images/overview.graffle +0 -0
  327. data/brutrb.com/images/overview.png +0 -0
  328. data/brutrb.com/images/spa.dot +0 -19
  329. data/brutrb.com/images/spa.png +0 -0
  330. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element-styled.png +0 -0
  331. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element.png +0 -0
  332. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser.png +0 -0
  333. data/brutrb.com/images/tutorial/02-confirmation-flow.graffle +0 -0
  334. data/brutrb.com/images/tutorial/02-confirmation-flow.png +0 -0
  335. data/brutrb.com/images/tutorial/basic-form-with-violations.png +0 -0
  336. data/brutrb.com/images/tutorial/basic-form.png +0 -0
  337. data/brutrb.com/images/tutorial/initial-home-page.png +0 -0
  338. data/brutrb.com/images/tutorial/new-post-editor.png +0 -0
  339. data/brutrb.com/images/tutorial/new-post-home-page.png +0 -0
  340. data/brutrb.com/images/tutorial/styled-form-with-server-side-violations.png +0 -0
  341. data/brutrb.com/images/tutorial/styled-form-with-violations.png +0 -0
  342. data/brutrb.com/images/tutorial/styled-home-page-with-posts.png +0 -0
  343. data/brutrb.com/images/tutorial/styled-home-page.png +0 -0
  344. data/brutrb.com/images/tutorial/welcome-to-brut.png +0 -0
  345. data/brutrb.com/images/workspace-protocol.dot +0 -44
  346. data/brutrb.com/images/workspace-protocol.png +0 -0
  347. data/brutrb.com/index.md +0 -34
  348. data/brutrb.com/instrumentation.md +0 -331
  349. data/brutrb.com/javascript.md +0 -122
  350. data/brutrb.com/jobs.md +0 -114
  351. data/brutrb.com/keyword-injection.md +0 -195
  352. data/brutrb.com/layouts.md +0 -156
  353. data/brutrb.com/lsp.md +0 -23
  354. data/brutrb.com/markdown-examples.md +0 -85
  355. data/brutrb.com/middleware.md +0 -80
  356. data/brutrb.com/overview.md +0 -68
  357. data/brutrb.com/package-lock.json +0 -2451
  358. data/brutrb.com/package.json +0 -11
  359. data/brutrb.com/pages.md +0 -290
  360. data/brutrb.com/public/SocialImage.png +0 -0
  361. data/brutrb.com/public/favicon.ico +0 -0
  362. data/brutrb.com/recipes/alternate-layouts.md +0 -32
  363. data/brutrb.com/recipes/authentication.md +0 -336
  364. data/brutrb.com/recipes/custom-flash.md +0 -51
  365. data/brutrb.com/recipes/dev-env-secrets.md +0 -87
  366. data/brutrb.com/recipes/form-errors.md +0 -148
  367. data/brutrb.com/recipes/indexed-forms.md +0 -149
  368. data/brutrb.com/recipes/migrations.md +0 -210
  369. data/brutrb.com/recipes/text-field-component.md +0 -182
  370. data/brutrb.com/roadmap.md +0 -52
  371. data/brutrb.com/routes.md +0 -189
  372. data/brutrb.com/security.md +0 -102
  373. data/brutrb.com/seed-data.md +0 -63
  374. data/brutrb.com/space-time-continuum.md +0 -81
  375. data/brutrb.com/tutorial.md +0 -138
  376. data/brutrb.com/tutorials/01-intro.md +0 -1654
  377. data/brutrb.com/tutorials/02-dialog.md +0 -569
  378. data/brutrb.com/unit-tests.md +0 -148
  379. data/brutrb.com/why.md +0 -19
  380. data/docker-compose.dx.yml +0 -25
  381. data/docs/404.html +0 -26
  382. data/docs/CNAME +0 -1
  383. data/docs/SocialImage.png +0 -0
  384. data/docs/adrs.html +0 -29
  385. data/docs/ai.html +0 -29
  386. data/docs/api/Brut/BackEnd/SeedData.html +0 -493
  387. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +0 -214
  388. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +0 -125
  389. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +0 -125
  390. data/docs/api/Brut/BackEnd/Sidekiq.html +0 -125
  391. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +0 -414
  392. data/docs/api/Brut/BackEnd/Validators.html +0 -128
  393. data/docs/api/Brut/BackEnd.html +0 -132
  394. data/docs/api/Brut/CLI/App.html +0 -1601
  395. data/docs/api/Brut/CLI/AppRunner.html +0 -491
  396. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +0 -264
  397. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +0 -306
  398. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +0 -262
  399. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +0 -314
  400. data/docs/api/Brut/CLI/Apps/BuildAssets.html +0 -183
  401. data/docs/api/Brut/CLI/Apps/DB/Create.html +0 -365
  402. data/docs/api/Brut/CLI/Apps/DB/Drop.html +0 -357
  403. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +0 -389
  404. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +0 -339
  405. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +0 -329
  406. data/docs/api/Brut/CLI/Apps/DB/Seed.html +0 -347
  407. data/docs/api/Brut/CLI/Apps/DB/Status.html +0 -383
  408. data/docs/api/Brut/CLI/Apps/DB.html +0 -183
  409. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +0 -270
  410. data/docs/api/Brut/CLI/Apps/DeployBase.html +0 -257
  411. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +0 -587
  412. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +0 -196
  413. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +0 -303
  414. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +0 -508
  415. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +0 -398
  416. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +0 -374
  417. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +0 -384
  418. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +0 -410
  419. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +0 -262
  420. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +0 -303
  421. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +0 -480
  422. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +0 -450
  423. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +0 -380
  424. data/docs/api/Brut/CLI/Apps/Scaffold.html +0 -253
  425. data/docs/api/Brut/CLI/Apps/Test/Audit.html +0 -474
  426. data/docs/api/Brut/CLI/Apps/Test/E2e.html +0 -407
  427. data/docs/api/Brut/CLI/Apps/Test/JS.html +0 -262
  428. data/docs/api/Brut/CLI/Apps/Test/Run.html +0 -578
  429. data/docs/api/Brut/CLI/Apps/Test.html +0 -253
  430. data/docs/api/Brut/CLI/Apps.html +0 -125
  431. data/docs/api/Brut/CLI/Command.html +0 -2425
  432. data/docs/api/Brut/CLI/Error.html +0 -139
  433. data/docs/api/Brut/CLI/ExecutionResults/Result.html +0 -664
  434. data/docs/api/Brut/CLI/ExecutionResults.html +0 -675
  435. data/docs/api/Brut/CLI/Executor.html +0 -561
  436. data/docs/api/Brut/CLI/InvalidOption.html +0 -245
  437. data/docs/api/Brut/CLI/Options.html +0 -880
  438. data/docs/api/Brut/CLI/Output.html +0 -699
  439. data/docs/api/Brut/CLI/SystemExecError.html +0 -451
  440. data/docs/api/Brut/CLI.html +0 -263
  441. data/docs/api/Brut/FactoryBot.html +0 -225
  442. data/docs/api/Brut/Framework/App.html +0 -1097
  443. data/docs/api/Brut/Framework/Config.html +0 -1071
  444. data/docs/api/Brut/Framework/Container.html +0 -1464
  445. data/docs/api/Brut/Framework/Error.html +0 -140
  446. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +0 -232
  447. data/docs/api/Brut/Framework/Errors/Bug.html +0 -234
  448. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +0 -257
  449. data/docs/api/Brut/Framework/Errors/MissingParameter.html +0 -273
  450. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +0 -471
  451. data/docs/api/Brut/Framework/Errors/NotFound.html +0 -308
  452. data/docs/api/Brut/Framework/Errors/NotImplemented.html +0 -234
  453. data/docs/api/Brut/Framework/Errors.html +0 -351
  454. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +0 -392
  455. data/docs/api/Brut/Framework/MCP.html +0 -871
  456. data/docs/api/Brut/Framework/ProjectEnvironment.html +0 -648
  457. data/docs/api/Brut/Framework.html +0 -129
  458. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +0 -317
  459. data/docs/api/Brut/FrontEnd/Component/Helpers.html +0 -420
  460. data/docs/api/Brut/FrontEnd/Component.html +0 -434
  461. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +0 -491
  462. data/docs/api/Brut/FrontEnd/Components/FormTag.html +0 -526
  463. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +0 -313
  464. data/docs/api/Brut/FrontEnd/Components/Input.html +0 -195
  465. data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +0 -447
  466. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +0 -339
  467. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +0 -568
  468. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +0 -419
  469. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +0 -610
  470. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +0 -534
  471. data/docs/api/Brut/FrontEnd/Components/Inputs.html +0 -125
  472. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +0 -367
  473. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +0 -355
  474. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +0 -655
  475. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +0 -352
  476. data/docs/api/Brut/FrontEnd/Components.html +0 -156
  477. data/docs/api/Brut/FrontEnd/CsrfProtector.html +0 -250
  478. data/docs/api/Brut/FrontEnd/Download.html +0 -467
  479. data/docs/api/Brut/FrontEnd/Flash.html +0 -1150
  480. data/docs/api/Brut/FrontEnd/Form.html +0 -1227
  481. data/docs/api/Brut/FrontEnd/Forms/Button.html +0 -331
  482. data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +0 -537
  483. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +0 -590
  484. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +0 -201
  485. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +0 -535
  486. data/docs/api/Brut/FrontEnd/Forms/Input.html +0 -1567
  487. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +0 -635
  488. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +0 -1336
  489. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +0 -730
  490. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +0 -587
  491. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +0 -734
  492. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +0 -582
  493. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +0 -659
  494. data/docs/api/Brut/FrontEnd/Forms.html +0 -127
  495. data/docs/api/Brut/FrontEnd/GenericResponse.html +0 -377
  496. data/docs/api/Brut/FrontEnd/Handler.html +0 -442
  497. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +0 -318
  498. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +0 -336
  499. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +0 -399
  500. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +0 -354
  501. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +0 -151
  502. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +0 -315
  503. data/docs/api/Brut/FrontEnd/Handlers.html +0 -125
  504. data/docs/api/Brut/FrontEnd/HandlingResults.html +0 -339
  505. data/docs/api/Brut/FrontEnd/HttpMethod.html +0 -661
  506. data/docs/api/Brut/FrontEnd/HttpStatus.html +0 -496
  507. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +0 -284
  508. data/docs/api/Brut/FrontEnd/Layout.html +0 -486
  509. data/docs/api/Brut/FrontEnd/Middleware.html +0 -135
  510. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +0 -288
  511. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +0 -292
  512. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +0 -324
  513. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +0 -376
  514. data/docs/api/Brut/FrontEnd/Middlewares.html +0 -125
  515. data/docs/api/Brut/FrontEnd/Page.html +0 -781
  516. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +0 -797
  517. data/docs/api/Brut/FrontEnd/Pages.html +0 -125
  518. data/docs/api/Brut/FrontEnd/RequestContext.html +0 -1312
  519. data/docs/api/Brut/FrontEnd/RouteHook.html +0 -424
  520. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +0 -242
  521. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +0 -249
  522. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +0 -264
  523. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +0 -261
  524. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +0 -284
  525. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +0 -252
  526. data/docs/api/Brut/FrontEnd/RouteHooks.html +0 -115
  527. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +0 -227
  528. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +0 -305
  529. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +0 -324
  530. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +0 -319
  531. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +0 -315
  532. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +0 -315
  533. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +0 -327
  534. data/docs/api/Brut/FrontEnd/Routing/Route.html +0 -761
  535. data/docs/api/Brut/FrontEnd/Routing.html +0 -927
  536. data/docs/api/Brut/FrontEnd/Session.html +0 -1195
  537. data/docs/api/Brut/FrontEnd.html +0 -134
  538. data/docs/api/Brut/I18n/BaseMethods.html +0 -931
  539. data/docs/api/Brut/I18n/ForBackEnd.html +0 -302
  540. data/docs/api/Brut/I18n/ForCLI.html +0 -302
  541. data/docs/api/Brut/I18n/ForHTML.html +0 -296
  542. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +0 -316
  543. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +0 -930
  544. data/docs/api/Brut/I18n.html +0 -127
  545. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +0 -435
  546. data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +0 -596
  547. data/docs/api/Brut/Instrumentation/Methods.html +0 -173
  548. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +0 -286
  549. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +0 -302
  550. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +0 -866
  551. data/docs/api/Brut/Instrumentation.html +0 -128
  552. data/docs/api/Brut/RubocopConfig.html +0 -237
  553. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +0 -534
  554. data/docs/api/Brut/SinatraHelpers.html +0 -281
  555. data/docs/api/Brut/SpecSupport/ClockSupport.html +0 -383
  556. data/docs/api/Brut/SpecSupport/ComponentSupport.html +0 -496
  557. data/docs/api/Brut/SpecSupport/E2ETestServer.html +0 -503
  558. data/docs/api/Brut/SpecSupport/E2eSupport.html +0 -142
  559. data/docs/api/Brut/SpecSupport/EnhancedNode.html +0 -403
  560. data/docs/api/Brut/SpecSupport/FlashSupport.html +0 -278
  561. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +0 -401
  562. data/docs/api/Brut/SpecSupport/GeneralSupport.html +0 -195
  563. data/docs/api/Brut/SpecSupport/HandlerSupport.html +0 -160
  564. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +0 -142
  565. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +0 -142
  566. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +0 -155
  567. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +0 -583
  568. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +0 -149
  569. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +0 -466
  570. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +0 -149
  571. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +0 -149
  572. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +0 -165
  573. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +0 -158
  574. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +0 -156
  575. data/docs/api/Brut/SpecSupport/Matchers.html +0 -125
  576. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +0 -335
  577. data/docs/api/Brut/SpecSupport/RSpecSetup.html +0 -637
  578. data/docs/api/Brut/SpecSupport/SessionSupport.html +0 -196
  579. data/docs/api/Brut/SpecSupport.html +0 -129
  580. data/docs/api/Brut/TUI/AnsiEscapeCode/Mod.html +0 -409
  581. data/docs/api/Brut/TUI/AnsiEscapeCode.html +0 -426
  582. data/docs/api/Brut/TUI/EventLoop/Deque.html +0 -531
  583. data/docs/api/Brut/TUI/EventLoop.html +0 -676
  584. data/docs/api/Brut/TUI/Events/BaseEvent.html +0 -449
  585. data/docs/api/Brut/TUI/Events/EventBus.html +0 -485
  586. data/docs/api/Brut/TUI/Events/EventLoopStarted.html +0 -211
  587. data/docs/api/Brut/TUI/Events/Exception.html +0 -523
  588. data/docs/api/Brut/TUI/Events/Tick.html +0 -294
  589. data/docs/api/Brut/TUI/Events.html +0 -131
  590. data/docs/api/Brut/TUI/MarkupString.html +0 -537
  591. data/docs/api/Brut/TUI/Script/BlockStep.html +0 -300
  592. data/docs/api/Brut/TUI/Script/Events/CommandExecutionFailed.html +0 -252
  593. data/docs/api/Brut/TUI/Script/Events/CommandExecutionSucceeded.html +0 -163
  594. data/docs/api/Brut/TUI/Script/Events/CommandStdErr.html +0 -163
  595. data/docs/api/Brut/TUI/Script/Events/CommandStdOut.html +0 -300
  596. data/docs/api/Brut/TUI/Script/Events/ExecutingCommand.html +0 -298
  597. data/docs/api/Brut/TUI/Script/Events/Message.html +0 -345
  598. data/docs/api/Brut/TUI/Script/Events/PhaseCompleted.html +0 -229
  599. data/docs/api/Brut/TUI/Script/Events/PhaseStarted.html +0 -350
  600. data/docs/api/Brut/TUI/Script/Events/ScriptCompleted.html +0 -282
  601. data/docs/api/Brut/TUI/Script/Events/ScriptStarted.html +0 -343
  602. data/docs/api/Brut/TUI/Script/Events/StepCompleted.html +0 -163
  603. data/docs/api/Brut/TUI/Script/Events/StepStarted.html +0 -346
  604. data/docs/api/Brut/TUI/Script/Events.html +0 -115
  605. data/docs/api/Brut/TUI/Script/ExecStep/ProcessStatusFailed.html +0 -210
  606. data/docs/api/Brut/TUI/Script/ExecStep.html +0 -493
  607. data/docs/api/Brut/TUI/Script/LoggingSubscriber.html +0 -914
  608. data/docs/api/Brut/TUI/Script/PutsSubscriber.html +0 -783
  609. data/docs/api/Brut/TUI/Script/Step.html +0 -313
  610. data/docs/api/Brut/TUI/Script.html +0 -1250
  611. data/docs/api/Brut/TUI/Terminal.html +0 -593
  612. data/docs/api/Brut/TUI/TerminalTheme.html +0 -1403
  613. data/docs/api/Brut/TUI/Themes/Dark.html +0 -706
  614. data/docs/api/Brut/TUI/Themes/Light.html +0 -804
  615. data/docs/api/Brut/TUI/Themes/None.html +0 -218
  616. data/docs/api/Brut/TUI/Themes.html +0 -115
  617. data/docs/api/Brut/TUI.html +0 -129
  618. data/docs/api/Brut.html +0 -341
  619. data/docs/api/Clock.html +0 -603
  620. data/docs/api/ModuleName.html +0 -595
  621. data/docs/api/RichString.html +0 -775
  622. data/docs/api/SemanticLogger/Appender/Async.html +0 -219
  623. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +0 -119
  624. data/docs/api/Sequel/Extensions/BrutMigrations.html +0 -541
  625. data/docs/api/Sequel/Extensions.html +0 -117
  626. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +0 -105
  627. data/docs/api/Sequel/Plugins/CreatedAt.html +0 -125
  628. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +0 -207
  629. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +0 -186
  630. data/docs/api/Sequel/Plugins/ExternalId.html +0 -218
  631. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +0 -202
  632. data/docs/api/Sequel/Plugins/FindBang.html +0 -125
  633. data/docs/api/Sequel/Plugins.html +0 -117
  634. data/docs/api/Sequel.html +0 -117
  635. data/docs/api/SpecSupport/Matchers/BeABug.html +0 -143
  636. data/docs/api/_index.html +0 -1964
  637. data/docs/api/class_list.html +0 -54
  638. data/docs/api/css/common.css +0 -1
  639. data/docs/api/css/full_list.css +0 -59
  640. data/docs/api/css/style.css +0 -504
  641. data/docs/api/file.README.html +0 -172
  642. data/docs/api/file_list.html +0 -59
  643. data/docs/api/frames.html +0 -22
  644. data/docs/api/index.html +0 -172
  645. data/docs/api/js/app.js +0 -344
  646. data/docs/api/js/full_list.js +0 -242
  647. data/docs/api/js/jquery.js +0 -4
  648. data/docs/api/method_list.html +0 -5542
  649. data/docs/api/top-level-namespace.html +0 -112
  650. data/docs/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png +0 -0
  651. data/docs/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png +0 -0
  652. data/docs/assets/02-confirmation-dialog-browser.DH8ALFO4.png +0 -0
  653. data/docs/assets/02-confirmation-flow.D9gZ0S5U.png +0 -0
  654. data/docs/assets/DevEnvironment.DaFcVfwP.png +0 -0
  655. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  656. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  657. data/docs/assets/adrs.md.YglbWtQe.js +0 -1
  658. data/docs/assets/adrs.md.YglbWtQe.lean.js +0 -1
  659. data/docs/assets/ai.md.ChLnvDAX.js +0 -1
  660. data/docs/assets/ai.md.ChLnvDAX.lean.js +0 -1
  661. data/docs/assets/app.B8jAEB7R.js +0 -1
  662. data/docs/assets/assets.md.BEF6Oz6K.js +0 -19
  663. data/docs/assets/assets.md.BEF6Oz6K.lean.js +0 -1
  664. data/docs/assets/basic-form-with-violations.Cv6Y9-Q_.png +0 -0
  665. data/docs/assets/basic-form.DbHnu0oW.png +0 -0
  666. data/docs/assets/brut-js.md.BMz0X1Rz.js +0 -12
  667. data/docs/assets/brut-js.md.BMz0X1Rz.lean.js +0 -1
  668. data/docs/assets/business-logic.md.DbuaOYGU.js +0 -1
  669. data/docs/assets/business-logic.md.DbuaOYGU.lean.js +0 -1
  670. data/docs/assets/chunks/@localSearchIndexroot.DJ8mocCj.js +0 -1
  671. data/docs/assets/chunks/VPLocalSearchBox.gF-Po_fz.js +0 -8
  672. data/docs/assets/chunks/framework.C4nOkCZI.js +0 -18
  673. data/docs/assets/chunks/theme.BjPAOJkz.js +0 -2
  674. data/docs/assets/cli.md.DDMar_51.js +0 -122
  675. data/docs/assets/cli.md.DDMar_51.lean.js +0 -1
  676. data/docs/assets/components.md.Ber8UBM0.js +0 -96
  677. data/docs/assets/components.md.Ber8UBM0.lean.js +0 -1
  678. data/docs/assets/configuration.md.DrJ6YVoZ.js +0 -78
  679. data/docs/assets/configuration.md.DrJ6YVoZ.lean.js +0 -1
  680. data/docs/assets/css.md.K5rOCOQY.js +0 -21
  681. data/docs/assets/css.md.K5rOCOQY.lean.js +0 -1
  682. data/docs/assets/custom-element-tests.md.DiLe-eFw.js +0 -69
  683. data/docs/assets/custom-element-tests.md.DiLe-eFw.lean.js +0 -1
  684. data/docs/assets/database-access.md.Dc8l2Plf.js +0 -63
  685. data/docs/assets/database-access.md.Dc8l2Plf.lean.js +0 -1
  686. data/docs/assets/database-schema.md.BJ_JhXmO.js +0 -70
  687. data/docs/assets/database-schema.md.BJ_JhXmO.lean.js +0 -1
  688. data/docs/assets/deployment.md.CHTx2eTR.js +0 -55
  689. data/docs/assets/deployment.md.CHTx2eTR.lean.js +0 -1
  690. data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
  691. data/docs/assets/dev-environment.md.B1S9p5ZK.js +0 -16
  692. data/docs/assets/dev-environment.md.B1S9p5ZK.lean.js +0 -1
  693. data/docs/assets/dir-structure.md.D1T2kGwj.js +0 -46
  694. data/docs/assets/dir-structure.md.D1T2kGwj.lean.js +0 -1
  695. data/docs/assets/doc-conventions.md.CDnWaEFg.js +0 -1
  696. data/docs/assets/doc-conventions.md.CDnWaEFg.lean.js +0 -1
  697. data/docs/assets/end-to-end-tests.md.BJJdNDYL.js +0 -28
  698. data/docs/assets/end-to-end-tests.md.BJJdNDYL.lean.js +0 -1
  699. data/docs/assets/features.md.BDWxnyNO.js +0 -154
  700. data/docs/assets/features.md.BDWxnyNO.lean.js +0 -1
  701. data/docs/assets/flash-and-session.md.CUsMxoNl.js +0 -79
  702. data/docs/assets/flash-and-session.md.CUsMxoNl.lean.js +0 -1
  703. data/docs/assets/form-constraints.md.KlfXSKm2.js +0 -90
  704. data/docs/assets/form-constraints.md.KlfXSKm2.lean.js +0 -1
  705. data/docs/assets/forms.md.RK0zkhm0.js +0 -64
  706. data/docs/assets/forms.md.RK0zkhm0.lean.js +0 -1
  707. data/docs/assets/getting-started.md.CGJ44juQ.js +0 -31
  708. data/docs/assets/getting-started.md.CGJ44juQ.lean.js +0 -1
  709. data/docs/assets/handlers.md.C5tUwmmo.js +0 -54
  710. data/docs/assets/handlers.md.C5tUwmmo.lean.js +0 -1
  711. data/docs/assets/hooks.md.CoiYCKRc.js +0 -80
  712. data/docs/assets/hooks.md.CoiYCKRc.lean.js +0 -1
  713. data/docs/assets/i18n.md.DxkCKhUw.js +0 -23
  714. data/docs/assets/i18n.md.DxkCKhUw.lean.js +0 -1
  715. data/docs/assets/index.md.DnphWyQd.js +0 -1
  716. data/docs/assets/index.md.DnphWyQd.lean.js +0 -1
  717. data/docs/assets/initial-home-page.DNIaYmgP.png +0 -0
  718. data/docs/assets/instrumentation.md.BcxjC4jd.js +0 -90
  719. data/docs/assets/instrumentation.md.BcxjC4jd.lean.js +0 -1
  720. data/docs/assets/javascript.md.D6fxhaQb.js +0 -31
  721. data/docs/assets/javascript.md.D6fxhaQb.lean.js +0 -1
  722. data/docs/assets/jobs.md.Bi3qb3v6.js +0 -25
  723. data/docs/assets/jobs.md.Bi3qb3v6.lean.js +0 -1
  724. data/docs/assets/keyword-injection.md.CqLnnzIz.js +0 -21
  725. data/docs/assets/keyword-injection.md.CqLnnzIz.lean.js +0 -1
  726. data/docs/assets/layouts.md.HEbeK7Jr.js +0 -68
  727. data/docs/assets/layouts.md.HEbeK7Jr.lean.js +0 -1
  728. data/docs/assets/lsp.md.bE9dW8n9.js +0 -1
  729. data/docs/assets/lsp.md.bE9dW8n9.lean.js +0 -1
  730. data/docs/assets/markdown-examples.md.BPmtHlc-.js +0 -33
  731. data/docs/assets/markdown-examples.md.BPmtHlc-.lean.js +0 -1
  732. data/docs/assets/middleware.md.BhOIsg59.js +0 -20
  733. data/docs/assets/middleware.md.BhOIsg59.lean.js +0 -1
  734. data/docs/assets/new-post-editor.DrHr-5oh.png +0 -0
  735. data/docs/assets/new-post-home-page.Bm34lyMg.png +0 -0
  736. data/docs/assets/overview.md.BpWAgPFH.js +0 -1
  737. data/docs/assets/overview.md.BpWAgPFH.lean.js +0 -1
  738. data/docs/assets/pages.md.B3sQXpEd.js +0 -45
  739. data/docs/assets/pages.md.B3sQXpEd.lean.js +0 -1
  740. data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.js +0 -22
  741. data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.lean.js +0 -1
  742. data/docs/assets/recipes_authentication.md.CyvoIW82.js +0 -157
  743. data/docs/assets/recipes_authentication.md.CyvoIW82.lean.js +0 -1
  744. data/docs/assets/recipes_custom-flash.md.6gFqf2uL.js +0 -26
  745. data/docs/assets/recipes_custom-flash.md.6gFqf2uL.lean.js +0 -1
  746. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.js +0 -12
  747. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.lean.js +0 -1
  748. data/docs/assets/recipes_form-errors.md.B5ptSzMO.js +0 -66
  749. data/docs/assets/recipes_form-errors.md.B5ptSzMO.lean.js +0 -1
  750. data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.js +0 -74
  751. data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.lean.js +0 -1
  752. data/docs/assets/recipes_migrations.md.Cid7-3cu.js +0 -97
  753. data/docs/assets/recipes_migrations.md.Cid7-3cu.lean.js +0 -1
  754. data/docs/assets/recipes_text-field-component.md.VhOsCtKI.js +0 -101
  755. data/docs/assets/recipes_text-field-component.md.VhOsCtKI.lean.js +0 -1
  756. data/docs/assets/roadmap.md.DqC1Y7Zt.js +0 -1
  757. data/docs/assets/roadmap.md.DqC1Y7Zt.lean.js +0 -1
  758. data/docs/assets/routes.md.C1dgIBtD.js +0 -21
  759. data/docs/assets/routes.md.C1dgIBtD.lean.js +0 -1
  760. data/docs/assets/security.md.Jn4SY1uK.js +0 -1
  761. data/docs/assets/security.md.Jn4SY1uK.lean.js +0 -1
  762. data/docs/assets/seed-data.md.UZW0WxYN.js +0 -14
  763. data/docs/assets/seed-data.md.UZW0WxYN.lean.js +0 -1
  764. data/docs/assets/spa.qejUdp-5.png +0 -0
  765. data/docs/assets/space-time-continuum.md.D9rYGDFH.js +0 -1
  766. data/docs/assets/space-time-continuum.md.D9rYGDFH.lean.js +0 -1
  767. data/docs/assets/style.B1z60PPQ.css +0 -1
  768. data/docs/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png +0 -0
  769. data/docs/assets/styled-form-with-violations.Bv_sa9tg.png +0 -0
  770. data/docs/assets/styled-home-page-with-posts.Dd4kG89D.png +0 -0
  771. data/docs/assets/styled-home-page.BzdI7dWz.png +0 -0
  772. data/docs/assets/tutorial.md.BX6f6l00.js +0 -27
  773. data/docs/assets/tutorial.md.BX6f6l00.lean.js +0 -1
  774. data/docs/assets/tutorials_01-intro.md.CzZ3kpF_.js +0 -708
  775. data/docs/assets/tutorials_01-intro.md.CzZ3kpF_.lean.js +0 -1
  776. data/docs/assets/tutorials_02-dialog.md.DE5WfCXI.js +0 -274
  777. data/docs/assets/tutorials_02-dialog.md.DE5WfCXI.lean.js +0 -1
  778. data/docs/assets/unit-tests.md.vDsdBbO_.js +0 -13
  779. data/docs/assets/unit-tests.md.vDsdBbO_.lean.js +0 -1
  780. data/docs/assets/welcome-to-brut.VSWzl17-.png +0 -0
  781. data/docs/assets/why.md.4WpxdrQ2.js +0 -1
  782. data/docs/assets/why.md.4WpxdrQ2.lean.js +0 -1
  783. data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
  784. data/docs/assets.html +0 -47
  785. data/docs/brut-css/brut.css +0 -1
  786. data/docs/brut-css/brut.max.css +0 -22372
  787. data/docs/brut-css/classes/appearances.html +0 -783
  788. data/docs/brut-css/classes/background-colors.html +0 -3529
  789. data/docs/brut-css/classes/border-colors.html +0 -3529
  790. data/docs/brut-css/classes/borders.html +0 -2293
  791. data/docs/brut-css/classes/dimensions.html +0 -2581
  792. data/docs/brut-css/classes/flex.html +0 -917
  793. data/docs/brut-css/classes/foreground-colors.html +0 -3261
  794. data/docs/brut-css/classes/junk-drawer.html +0 -431
  795. data/docs/brut-css/classes/layout.html +0 -668
  796. data/docs/brut-css/classes/lists.html +0 -331
  797. data/docs/brut-css/classes/positioning.html +0 -1751
  798. data/docs/brut-css/classes/spacings.html +0 -2633
  799. data/docs/brut-css/classes/typography.html +0 -2206
  800. data/docs/brut-css/customization/advanced-configuration.html +0 -204
  801. data/docs/brut-css/customization/breakpoints.html +0 -227
  802. data/docs/brut-css/customization/design-system.html +0 -197
  803. data/docs/brut-css/customization/pseudo-classes.html +0 -228
  804. data/docs/brut-css/docs.css +0 -98
  805. data/docs/brut-css/getting-started/core-concepts.html +0 -234
  806. data/docs/brut-css/getting-started/installation.html +0 -190
  807. data/docs/brut-css/getting-started/overview.html +0 -210
  808. data/docs/brut-css/getting-started/simple-example.html +0 -285
  809. data/docs/brut-css/index.html +0 -193
  810. data/docs/brut-css/prism-twilight.min.css +0 -1
  811. data/docs/brut-css/properties/colors.html +0 -1548
  812. data/docs/brut-css/properties/spacings.html +0 -614
  813. data/docs/brut-css/properties/typography.html +0 -777
  814. data/docs/brut-js/api/AjaxSubmit.html +0 -452
  815. data/docs/brut-js/api/AjaxSubmit.js.html +0 -550
  816. data/docs/brut-js/api/Autosubmit.html +0 -192
  817. data/docs/brut-js/api/Autosubmit.js.html +0 -114
  818. data/docs/brut-js/api/BaseCustomElement.html +0 -1091
  819. data/docs/brut-js/api/BaseCustomElement.js.html +0 -312
  820. data/docs/brut-js/api/BrutCustomElements.html +0 -172
  821. data/docs/brut-js/api/BufferedLogger.html +0 -173
  822. data/docs/brut-js/api/ConfirmSubmit.html +0 -286
  823. data/docs/brut-js/api/ConfirmSubmit.js.html +0 -188
  824. data/docs/brut-js/api/ConfirmationDialog.html +0 -425
  825. data/docs/brut-js/api/ConfirmationDialog.js.html +0 -194
  826. data/docs/brut-js/api/ConstraintViolationMessage.html +0 -498
  827. data/docs/brut-js/api/ConstraintViolationMessage.js.html +0 -191
  828. data/docs/brut-js/api/ConstraintViolationMessages.html +0 -590
  829. data/docs/brut-js/api/ConstraintViolationMessages.js.html +0 -149
  830. data/docs/brut-js/api/CopyToClipboard.html +0 -345
  831. data/docs/brut-js/api/CopyToClipboard.js.html +0 -147
  832. data/docs/brut-js/api/Form.html +0 -291
  833. data/docs/brut-js/api/Form.js.html +0 -198
  834. data/docs/brut-js/api/I18nTranslation.html +0 -409
  835. data/docs/brut-js/api/I18nTranslation.js.html +0 -115
  836. data/docs/brut-js/api/LocaleDetection.html +0 -312
  837. data/docs/brut-js/api/LocaleDetection.js.html +0 -168
  838. data/docs/brut-js/api/Logger.html +0 -702
  839. data/docs/brut-js/api/Logger.js.html +0 -141
  840. data/docs/brut-js/api/Message.html +0 -238
  841. data/docs/brut-js/api/Message.js.html +0 -113
  842. data/docs/brut-js/api/PrefixedLogger.html +0 -369
  843. data/docs/brut-js/api/RichString.html +0 -1049
  844. data/docs/brut-js/api/RichString.js.html +0 -167
  845. data/docs/brut-js/api/Tabs.html +0 -295
  846. data/docs/brut-js/api/Tabs.js.html +0 -219
  847. data/docs/brut-js/api/Toast.html +0 -270
  848. data/docs/brut-js/api/Toast.js.html +0 -153
  849. data/docs/brut-js/api/Tracing.html +0 -277
  850. data/docs/brut-js/api/Tracing.js.html +0 -298
  851. data/docs/brut-js/api/external-CustomElementRegistry.html +0 -140
  852. data/docs/brut-js/api/external-Performance.html +0 -138
  853. data/docs/brut-js/api/external-Promise.html +0 -138
  854. data/docs/brut-js/api/external-ValidityState.html +0 -138
  855. data/docs/brut-js/api/external-Window.html +0 -233
  856. data/docs/brut-js/api/external-fetch.html +0 -138
  857. data/docs/brut-js/api/global.html +0 -400
  858. data/docs/brut-js/api/index.html +0 -168
  859. data/docs/brut-js/api/index.js.html +0 -184
  860. data/docs/brut-js/api/module-testing.html +0 -383
  861. data/docs/brut-js/api/scripts/linenumber.js +0 -25
  862. data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +0 -202
  863. data/docs/brut-js/api/scripts/prettify/lang-css.js +0 -2
  864. data/docs/brut-js/api/scripts/prettify/prettify.js +0 -28
  865. data/docs/brut-js/api/styles/jsdoc-default.css +0 -327
  866. data/docs/brut-js/api/styles/prettify-jsdoc.css +0 -111
  867. data/docs/brut-js/api/styles/prettify-tomorrow.css +0 -132
  868. data/docs/brut-js/api/testing.AssetMetadata.html +0 -172
  869. data/docs/brut-js/api/testing.AssetMetadataLoader.html +0 -171
  870. data/docs/brut-js/api/testing.CustomElementTest.html +0 -679
  871. data/docs/brut-js/api/testing.DOMCreator.html +0 -171
  872. data/docs/brut-js/api/testing_AssetMetadata.js.html +0 -86
  873. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +0 -76
  874. data/docs/brut-js/api/testing_CustomElementTest.js.html +0 -286
  875. data/docs/brut-js/api/testing_DOMCreator.js.html +0 -96
  876. data/docs/brut-js/api/testing_index.js.html +0 -99
  877. data/docs/brut-js.html +0 -40
  878. data/docs/business-logic.html +0 -29
  879. data/docs/cli.html +0 -150
  880. data/docs/components.html +0 -124
  881. data/docs/configuration.html +0 -106
  882. data/docs/css.html +0 -49
  883. data/docs/custom-element-tests.html +0 -97
  884. data/docs/database-access.html +0 -91
  885. data/docs/database-schema.html +0 -98
  886. data/docs/deployment.html +0 -83
  887. data/docs/dev-environment.html +0 -44
  888. data/docs/dir-structure.html +0 -74
  889. data/docs/doc-conventions.html +0 -29
  890. data/docs/end-to-end-tests.html +0 -56
  891. data/docs/favicon.ico +0 -0
  892. data/docs/features.html +0 -182
  893. data/docs/flash-and-session.html +0 -107
  894. data/docs/form-constraints.html +0 -118
  895. data/docs/forms.html +0 -92
  896. data/docs/getting-started.html +0 -59
  897. data/docs/handlers.html +0 -82
  898. data/docs/hashmap.json +0 -1
  899. data/docs/hooks.html +0 -108
  900. data/docs/i18n.html +0 -51
  901. data/docs/index.html +0 -29
  902. data/docs/instrumentation.html +0 -118
  903. data/docs/javascript.html +0 -59
  904. data/docs/jobs.html +0 -53
  905. data/docs/keyword-injection.html +0 -49
  906. data/docs/layouts.html +0 -96
  907. data/docs/lsp.html +0 -29
  908. data/docs/markdown-examples.html +0 -61
  909. data/docs/middleware.html +0 -48
  910. data/docs/overview.html +0 -29
  911. data/docs/pages.html +0 -73
  912. data/docs/recipes/alternate-layouts.html +0 -50
  913. data/docs/recipes/authentication.html +0 -185
  914. data/docs/recipes/custom-flash.html +0 -54
  915. data/docs/recipes/dev-env-secrets.html +0 -40
  916. data/docs/recipes/form-errors.html +0 -94
  917. data/docs/recipes/indexed-forms.html +0 -102
  918. data/docs/recipes/migrations.html +0 -125
  919. data/docs/recipes/text-field-component.html +0 -129
  920. data/docs/roadmap.html +0 -29
  921. data/docs/routes.html +0 -49
  922. data/docs/security.html +0 -29
  923. data/docs/seed-data.html +0 -42
  924. data/docs/space-time-continuum.html +0 -29
  925. data/docs/tutorial.html +0 -55
  926. data/docs/tutorials/01-intro.html +0 -736
  927. data/docs/tutorials/02-dialog.html +0 -302
  928. data/docs/unit-tests.html +0 -41
  929. data/docs/vp-icons.css +0 -1
  930. data/docs/why.html +0 -29
  931. data/docs-todo.md +0 -32
  932. data/dx/bash_customizations +0 -6
  933. data/dx/build +0 -73
  934. data/dx/build.pre +0 -15
  935. data/dx/docker-compose.env +0 -22
  936. data/dx/dx.sh.lib +0 -24
  937. data/dx/exec +0 -75
  938. data/dx/setupkit.sh.lib +0 -144
  939. data/dx/show-help-in-app-container-then-wait.sh +0 -38
  940. data/lib/brut/cli/app.rb +0 -238
  941. data/lib/brut/cli/app_runner.rb +0 -252
  942. data/lib/brut/cli/command.rb +0 -258
  943. data/lib/brut/cli/execution_results.rb +0 -119
  944. data/lib/brut/front_end/layouts/_internal.html.erb +0 -68
  945. data/lib/brut/front_end/pages/_missing_page.html.erb +0 -17
  946. data/mkbrut/.gitignore +0 -16
  947. data/mkbrut/CODE_OF_CONDUCT.txt +0 -100
  948. data/mkbrut/Gemfile +0 -3
  949. data/mkbrut/Gemfile.lock +0 -20
  950. data/mkbrut/LICENSE.txt +0 -370
  951. data/mkbrut/README.md +0 -145
  952. data/mkbrut/Rakefile +0 -2
  953. data/mkbrut/bin/build +0 -36
  954. data/mkbrut/bin/ci +0 -19
  955. data/mkbrut/bin/docs +0 -19
  956. data/mkbrut/bin/publish +0 -129
  957. data/mkbrut/bin/rake +0 -16
  958. data/mkbrut/bin/setup +0 -30
  959. data/mkbrut/brut-welcome.png +0 -0
  960. data/mkbrut/deploy/.dockerignore +0 -2
  961. data/mkbrut/deploy/Dockerfile +0 -25
  962. data/mkbrut/dx +0 -1
  963. data/mkbrut/exe/mkbrut +0 -5
  964. data/mkbrut/lib/mkbrut/app_name.rb +0 -29
  965. data/mkbrut/lib/mkbrut/app_options.rb +0 -36
  966. data/mkbrut/lib/mkbrut/cli.rb +0 -189
  967. data/mkbrut/lib/mkbrut/erb_binding_delegate.rb +0 -20
  968. data/mkbrut/lib/mkbrut/ops.rb +0 -17
  969. data/mkbrut/lib/mkbrut/organization.rb +0 -5
  970. data/mkbrut/lib/mkbrut/segments.rb +0 -8
  971. data/mkbrut/lib/mkbrut/version.rb +0 -3
  972. data/mkbrut/lib/mkbrut.rb +0 -20
  973. data/mkbrut/mkbrut.gemspec +0 -34
  974. data/mkbrut/templates/Base/app/src/front_end/images/LogoPylon.png +0 -0
  975. data/mkbrut/templates/Base/bin/build-assets +0 -7
  976. data/mkbrut/templates/Base/bin/ci +0 -39
  977. data/mkbrut/templates/Base/bin/db +0 -9
  978. data/mkbrut/templates/Base/bin/scaffold +0 -9
  979. data/mkbrut/templates/Base/bin/setup +0 -287
  980. data/mkbrut/templates/Base/bin/test +0 -9
  981. data/mkbrut/templates/Base/bin/test-server +0 -29
  982. data/mkbrut/templates/Base/dx/prune +0 -19
  983. data/mkbrut/templates/Base/dx/start +0 -30
  984. data/mkbrut/templates/Base/dx/stop +0 -23
  985. data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +0 -27
  986. data/specs/brut/front_end/forms/input.spec.rb +0 -978
  987. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +0 -54
  988. data/specs/brut/front_end/forms/select_input.spec.rb +0 -54
  989. data/specs/brut/instrumentation/methods.spec.rb +0 -399
  990. data/specs/brut/junk_drawer.spec.rb +0 -79
  991. data/specs/brut/tui/ansi_escape_code.spec.rb +0 -30
  992. data/specs/brut/tui/event_loop.spec.rb +0 -70
  993. data/specs/brut/tui/events/base_event.spec.rb +0 -26
  994. data/specs/brut/tui/events/event_bus.spec.rb +0 -141
  995. data/specs/brut/tui/events/exception.spec.rb +0 -19
  996. data/specs/brut/tui/events/test_event.rb +0 -5
  997. data/specs/spec_helper.rb +0 -31
  998. data/specs/support/matchers/have_constraint_violation.rb +0 -23
  999. data/specs/support/matchers.rb +0 -5
  1000. data/specs/support.rb +0 -3
  1001. /data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/prefixed_io.rb +0 -0
  1002. /data/{mkbrut/templates → templates}/Base/.dockerignore +0 -0
  1003. /data/{mkbrut/templates → templates}/Base/.env.development.erb +0 -0
  1004. /data/{mkbrut/templates → templates}/Base/.env.test.erb +0 -0
  1005. /data/{mkbrut/templates → templates}/Base/.gitignore +0 -0
  1006. /data/{mkbrut/templates → templates}/Base/.projections.json +0 -0
  1007. /data/{mkbrut/templates → templates}/Base/Dockerfile.dx +0 -0
  1008. /data/{mkbrut/templates → templates}/Base/Gemfile.erb +0 -0
  1009. /data/{mkbrut/templates → templates}/Base/Procfile.development +0 -0
  1010. /data/{mkbrut/templates → templates}/Base/Procfile.test +0 -0
  1011. /data/{mkbrut/templates → templates}/Base/README.md +0 -0
  1012. /data/{mkbrut/templates → templates}/Base/README.md.erb +0 -0
  1013. /data/{mkbrut/templates → templates}/Base/app/bootstrap.rb +0 -0
  1014. /data/{mkbrut/templates → templates}/Base/app/config/i18n/en/1_defaults.rb +0 -0
  1015. /data/{mkbrut/templates → templates}/Base/app/config/i18n/en/2_app.rb +0 -0
  1016. /data/{mkbrut/templates → templates}/Base/app/public/static/manifest.json.erb +0 -0
  1017. /data/{mkbrut/templates → templates}/Base/app/src/app.rb.erb +0 -0
  1018. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/app_data_model.rb +0 -0
  1019. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/db.rb +0 -0
  1020. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb +0 -0
  1021. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/seed/seed_data.rb +0 -0
  1022. /data/{mkbrut/templates → templates}/Base/app/src/front_end/components/app_component.rb +0 -0
  1023. /data/{mkbrut/templates → templates}/Base/app/src/front_end/components/custom_element_registration.rb.erb +0 -0
  1024. /data/{mkbrut/templates → templates}/Base/app/src/front_end/css/index.css +0 -0
  1025. /data/{mkbrut/templates → templates}/Base/app/src/front_end/css/svgs.css +0 -0
  1026. /data/{mkbrut/templates → templates}/Base/app/src/front_end/forms/app_form.rb +0 -0
  1027. /data/{mkbrut/templates → templates}/Base/app/src/front_end/handlers/app_handler.rb +0 -0
  1028. /data/{brutrb.com → templates/Base/app/src/front_end}/images/LogoPylon.png +0 -0
  1029. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/LogoTransit.png +0 -0
  1030. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
  1031. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
  1032. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
  1033. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
  1034. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/favicon.ico +0 -0
  1035. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/icon.png +0 -0
  1036. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/mkicons.sh +0 -0
  1037. /data/{mkbrut/templates → templates}/Base/app/src/front_end/js/index.js +0 -0
  1038. /data/{mkbrut/templates → templates}/Base/app/src/front_end/layouts/blank_layout.rb +0 -0
  1039. /data/{mkbrut/templates → templates}/Base/app/src/front_end/layouts/default_layout.rb.erb +0 -0
  1040. /data/{mkbrut/templates → templates}/Base/app/src/front_end/pages/app_page.rb +0 -0
  1041. /data/{mkbrut/templates → templates}/Base/app/src/front_end/pages/home_page.rb +0 -0
  1042. /data/{mkbrut/templates → templates}/Base/app/src/front_end/support/app_session.rb +0 -0
  1043. /data/{mkbrut/templates → templates}/Base/app/src/front_end/svgs/README.md +0 -0
  1044. /data/{mkbrut/templates → templates}/Base/app/src/front_end/svgs/comment-button.svg +0 -0
  1045. /data/{mkbrut/templates → templates}/Base/bin/README.md.erb +0 -0
  1046. /data/{mkbrut/templates → templates}/Base/bin/console +0 -0
  1047. /data/{mkbrut/templates → templates}/Base/bin/dbconsole +0 -0
  1048. /data/{mkbrut/templates → templates}/Base/bin/dev +0 -0
  1049. /data/{mkbrut/templates → templates}/Base/bin/run +0 -0
  1050. /data/{mkbrut/templates → templates}/Base/bin/run.run +0 -0
  1051. /data/{mkbrut/templates → templates}/Base/bin/startup-message +0 -0
  1052. /data/{mkbrut/templates → templates}/Base/config.ru +0 -0
  1053. /data/{mkbrut/templates → templates}/Base/docker-compose.dx.yml +0 -0
  1054. /data/{mkbrut/templates → templates}/Base/dx/README.md +0 -0
  1055. /data/{mkbrut/templates → templates}/Base/dx/bash_customizations +0 -0
  1056. /data/{mkbrut/templates → templates}/Base/dx/bash_customizations.local +0 -0
  1057. /data/{mkbrut/templates → templates}/Base/dx/build +0 -0
  1058. /data/{mkbrut/templates → templates}/Base/dx/dx.sh.lib +0 -0
  1059. /data/{mkbrut/templates → templates}/Base/dx/exec +0 -0
  1060. /data/{dx → templates/Base/dx}/prune +0 -0
  1061. /data/{mkbrut/templates → templates}/Base/dx/show-help-in-app-container-then-wait.sh +0 -0
  1062. /data/{dx → templates/Base/dx}/start +0 -0
  1063. /data/{dx → templates/Base/dx}/stop +0 -0
  1064. /data/{mkbrut/templates → templates}/Base/package.json.erb +0 -0
  1065. /data/{mkbrut/templates → templates}/Base/puma.config.rb +0 -0
  1066. /data/{mkbrut/templates → templates}/Base/specs/e2e/home_page.spec.rb.erb +0 -0
  1067. /data/{mkbrut/templates → templates}/Base/specs/front_end/js/SpecHelper.js +0 -0
  1068. /data/{mkbrut/templates → templates}/Base/specs/front_end/pages/home_page.spec.rb +0 -0
  1069. /data/{mkbrut/templates → templates}/Base/specs/lint_factories.spec.rb +0 -0
  1070. /data/{mkbrut/templates → templates}/Base/specs/spec_helper.rb +0 -0
  1071. /data/{mkbrut/templates → templates}/Base/specs/support.rb +0 -0
  1072. /data/{mkbrut/templates → templates}/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +0 -0
  1073. /data/{mkbrut/templates → templates}/segments/BareBones/app/src/front_end/js/Example.js.erb +0 -0
  1074. /data/{mkbrut/templates → templates}/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +0 -0
  1075. /data/{mkbrut/templates → templates}/segments/BareBones/specs/front_end/js/Example.spec.js.erb +0 -0
  1076. /data/{mkbrut/templates → templates}/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +0 -0
  1077. /data/{mkbrut/templates → templates}/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +0 -0
  1078. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/components/flash_component.rb +0 -0
  1079. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/css/constraint-violations.css +0 -0
  1080. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
  1081. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +0 -0
  1082. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +0 -0
  1083. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +0 -0
  1084. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/guestbook_page.rb +0 -0
  1085. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +0 -0
  1086. /data/{mkbrut/templates → templates}/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +0 -0
  1087. /data/{mkbrut/templates → templates}/segments/Demo/specs/e2e/guest_message.spec.rb +0 -0
  1088. /data/{mkbrut/templates → templates}/segments/Demo/specs/factories/db/guestbook_message.factory.rb +0 -0
  1089. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/components/flash_component.spec.rb +0 -0
  1090. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +0 -0
  1091. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +0 -0
  1092. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +0 -0
  1093. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +0 -0
  1094. /data/{mkbrut/templates → templates}/segments/Heroku/bin/deploy +0 -0
  1095. /data/{mkbrut/templates → templates}/segments/Heroku/deploy/docker-entrypoint +0 -0
  1096. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/boot_sidekiq.rb +0 -0
  1097. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/config/sidekiq.yml +0 -0
  1098. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/jobs/app_job.rb +0 -0
  1099. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/jobs/example_job.rb +0 -0
  1100. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/segments/sidekiq_segment.rb +0 -0
  1101. /data/{mkbrut/templates → templates}/segments/Sidekiq/bin/run.sidekiq +0 -0
  1102. /data/{mkbrut/templates → templates}/segments/Sidekiq/specs/back_end/jobs/example_job.spec.rb +0 -0
  1103. /data/{mkbrut/templates → templates}/segments/Sidekiq/specs/integration/sidekiq_works.spec.rb +0 -0
@@ -1,708 +0,0 @@
1
- import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const t="/assets/welcome-to-brut.VSWzl17-.png",p="/assets/initial-home-page.DNIaYmgP.png",l="/assets/styled-home-page.BzdI7dWz.png",h="/assets/basic-form.DbHnu0oW.png",o="/assets/basic-form-with-violations.Cv6Y9-Q_.png",k="/assets/styled-form-with-violations.Bv_sa9tg.png",d="/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png",r="/assets/styled-home-page-with-posts.Dd4kG89D.png",c="/assets/new-post-editor.DrHr-5oh.png",g="/assets/new-post-home-page.Bm34lyMg.png",B=JSON.parse('{"title":"Build a Blog in 15 Minutes","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/01-intro.md","filePath":"tutorials/01-intro.md"}'),E={name:"tutorials/01-intro.md"};function u(y,s,F,b,m,C){return n(),a("div",null,[...s[0]||(s[0]=[e(`<h1 id="build-a-blog-in-15-minutes" tabindex="-1">Build a Blog in 15 Minutes <a class="header-anchor" href="#build-a-blog-in-15-minutes" aria-label="Permalink to &quot;Build a Blog in 15 Minutes&quot;">​</a></h1><p>This will start from nothing and show you the main features of Brut by building a very basic blog. You&#39;ll learn how to make a new Brut app, how to build pages, submit forms, validate data, and access data in a database. You&#39;ll also learn how to test it all.</p><h2 id="set-up" tabindex="-1">Set Up <a class="header-anchor" href="#set-up" aria-label="Permalink to &quot;Set Up&quot;">​</a></h2><p>The only two pieces of software you need are Docker and a code editor:</p><ol><li><p><a href="https://docker.com" target="_blank" rel="noreferrer">Install Docker</a></p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you are on Windows, we <em>highly</em> recommend you use the Windows Subystem for Linux (WSL2), as this makes Brut, web developement, and, honestly, your entire life as you know it, far easier than trying to get things working natively in Windows.</p></div></li><li><p>If you are new to programming or new to Ruby and don&#39;t know what editor to get, use VSCode. If you are a vim or emacs person, those will be far better, but if you are used to an IDE, VSCode will be the easiest to get set up and learn to use.</p></li></ol><p>To check that docker is installed, open up a terminal and run:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> info</span></span></code></pre></div><p>This should produce a ton of output:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
2
- <span class="line"><span>Client:</span></span>
3
- <span class="line"><span> Version: 28.2.2</span></span>
4
- <span class="line"><span>«LOTS OF OUTPUT»</span></span></code></pre></div><p>To be extra sure, <strong>right after you ran <code>docker info</code></strong>, check <code>$?</code>, the exit code, to make sure it&#39;s a 0, which means the command ran successfully:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">echo</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> $?</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
5
- <span class="line"><span>0</span></span></code></pre></div><p>Now, let&#39;s create the app by first initializing it.</p><h2 id="initialize-your-app" tabindex="-1">Initialize Your App <a class="header-anchor" href="#initialize-your-app" aria-label="Permalink to &quot;Initialize Your App&quot;">​</a></h2><p><code>mkbrut</code> is a command line app that will initialize your new app. It&#39;s available as a RubyGem or a Docker image. We&#39;ll use the Docker image since that doesn&#39;t require installing anything.</p><p>We&#39;ll call the blog simply &quot;blog&quot;. <code>mkbrut</code> will insert some demo features in new apps to show you have to use Brut. Since you&#39;re following this tutorial, you don&#39;t need that, so we&#39;ll use the <code>--no-demo</code> flag.</p><p><code>cd</code> to a folder where you&#39;d like to work. <code>mkbrut</code> will create a folder called <code>blog</code> in there and in <em>that</em> folder, your app will be initialized.</p><p>The command to do this is pretty long, because it downloads <code>mkbrut</code> and then runs it inside a Docker container, meaning you don&#39;t have to install anything new. Here it is:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
6
- <span class="line"><span> --pull always \\</span></span>
7
- <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
8
- <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
9
- <span class="line"><span> -u $(id -u):$(id -g) \\</span></span>
10
- <span class="line"><span> -it \\</span></span>
11
- <span class="line"><span> thirdtank/mkbrut \\</span></span>
12
- <span class="line"><span> mkbrut --no-demo blog</span></span></code></pre></div><p>You should see this output:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
13
- <span class="line"><span>[ mkbrut ] Creating app with these options:</span></span>
14
- <span class="line"><span>[ mkbrut ] App name: blog</span></span>
15
- <span class="line"><span>[ mkbrut ] App ID: blog</span></span>
16
- <span class="line"><span>[ mkbrut ] Prefix: bl</span></span>
17
- <span class="line"><span>[ mkbrut ] Organization: blog</span></span>
18
- <span class="line"><span>[ mkbrut ] Include demo? false</span></span>
19
- <span class="line"><span>[ mkbrut ] Creating Base app</span></span>
20
- <span class="line"><span>[ mkbrut ] Creating segment: Bare bones framing</span></span>
21
- <span class="line"><span>[ mkbrut ] blog was created</span></span>
22
- <span class="line"><span></span></span>
23
- <span class="line"><span>[ mkbrut ] Time to get building:</span></span>
24
- <span class="line"><span>[ mkbrut ] 1. cd blog</span></span>
25
- <span class="line"><span>[ mkbrut ] 2. dx/build</span></span>
26
- <span class="line"><span>[ mkbrut ] 3. dx/start</span></span>
27
- <span class="line"><span>[ mkbrut ] 4. [ in another terminal ] dx/exec bash</span></span>
28
- <span class="line"><span>[ mkbrut ] 5. [ inside the Docker container ] bin/setup</span></span>
29
- <span class="line"><span>[ mkbrut ] 6. [ inside the Docker container ] bin/dev</span></span>
30
- <span class="line"><span>[ mkbrut ] 7. Visit http://localhost:6502 in your browser</span></span>
31
- <span class="line"><span>[ mkbrut ] 8. [ inside the Docker container ] bin/setup help # to see more commands</span></span></code></pre></div><p>Before we follow the instructions in the output, <code>cd</code> to <code>blog</code> and check it out.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">cd</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> blog</span></span>
32
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ls</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>#OUTPUT</span></span>
33
- <span class="line"><span>app Dockerfile.dx Procfile.development specs</span></span>
34
- <span class="line"><span>bin dx Procfile.test </span></span>
35
- <span class="line"><span>config.ru Gemfile puma.config.rb </span></span>
36
- <span class="line"><span>docker-compose.dx.yml package.json README.md</span></span></code></pre></div><ul><li><code>app</code> is where all the code your app will be</li><li><code>bin</code> has command line tools to manage your app</li><li><code>dx</code> has command line tools to manage your development environment</li><li><code>specs</code> is where your tests will go</li></ul><p>OK, let&#39;s start up the dev environment:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/build</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
37
- <span class="line"><span>[ dx/build ] Could not find Gemfile.lock, which is needed to determine the playwright-ruby-client version</span></span>
38
- <span class="line"><span>[ dx/build ] Assuming your app is brand-new, this should be OK</span></span>
39
- <span class="line"><span>[+] Building 0.2s</span></span>
40
- <span class="line"><span>«LOTS OF OUTPUT»</span></span></code></pre></div><p>This may take a while, but it&#39;s building a Docker image where all your work will happen, although you&#39;ll be able to edit your code on your computer with the editor of your choice.</p><p>When this is done, you should see a message like so:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
41
- <span class="line"><span>[ dx/build ] 🌈 Your Docker image has been built tagged &#39;blog/blog:ruby-3.4&#39;</span></span>
42
- <span class="line"><span>[ dx/build ] 🔄 You can now run dx/start to start it up, though you may need to stop it first with Ctrl-C</span></span></code></pre></div><p>Now, start up the environment:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/start</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>#OUTPUT</span></span>
43
- <span class="line"><span>[ dx/start ] 🚀 Starting docker-compose.dx.yml</span></span>
44
- <span class="line"><span>[+] Running 1/5</span></span>
45
- <span class="line"><span> ⠙ Container blog-postgres-1</span></span>
46
- <span class="line"><span> ⠙ Container blog-app-1</span></span>
47
- <span class="line"><span> ⠙ Container blog-otel-desktop-viewer-1</span></span>
48
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
49
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.568390000-04:00</span></span>
50
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.568978000-04:00</span></span>
51
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.569430000-04:00</span></span>
52
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.569825000-04:00 🎉 Dev Environment Initialized! 🎉</span></span>
53
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.570214000-04:00</span></span>
54
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.570599000-04:00 ℹ️ To use this environment, open a new terminal and run</span></span>
55
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.570980000-04:00</span></span>
56
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.571250000-04:00 dx/exec bash</span></span>
57
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.571521000-04:00</span></span>
58
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.571795000-04:00 🕹 Use \`ctrl-c\` to exit.</span></span>
59
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.572064000-04:00</span></span>
60
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.572327000-04:00</span></span>
61
- <span class="line"><span>app-1 | 2025-08-11T16:39:11.572596000-04:00</span></span></code></pre></div><p><code>dx/start</code> will keep running. If you stop it, your dev environment will stop. It&#39;s running three containers:</p><ul><li><code>app</code>, which is where the app and its test will run</li><li><code>postgres</code>, which is running PostgreSQL, a SQL database you&#39;ll use</li><li><code>otel-desktop-viewer</code> which can view telemetry of your app. We&#39;ll see that later.</li></ul><p>Now, let&#39;s access the container we started.</p><p>Open a new terminal window, <code>cd</code> to where <code>blog</code> is, and use <code>dx/exec</code> to run <code>bash</code>, effectively &quot;logging in&quot; to the container:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/exec</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> bash</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
62
- <span class="line"><span>[ dx/exec ] 🚂 Running &#39;ssh-agent bash&#39; inside container with service name &#39;app&#39;</span></span>
63
- <span class="line"><span>Now using node v22.18.0 (npm v10.9.3)</span></span>
64
- <span class="line"><span>docker-container - Projects/blog</span></span>
65
- <span class="line"><span>&gt;</span></span></code></pre></div><p>At that prompt, you can now type commands. If you type <code>ls</code>, you&#39;ll see the same files you saw when we ran it above:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ls</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>#OUTPUT</span></span>
66
- <span class="line"><span>app Dockerfile.dx Procfile.development specs</span></span>
67
- <span class="line"><span>bin dx Procfile.test </span></span>
68
- <span class="line"><span>config.ru Gemfile puma.config.rb </span></span>
69
- <span class="line"><span>docker-compose.dx.yml package.json README.md</span></span></code></pre></div><p>This is because the folder on your computer is synced to the one inside the container. Changes in one are immediately reflected in the other.</p><p><strong>From here on out, all command line invocations are run inside this container</strong>, unless stated otherwise.</p><h2 id="set-up-the-app-itself" tabindex="-1">Set Up the App Itself <a class="header-anchor" href="#set-up-the-app-itself" aria-label="Permalink to &quot;Set Up the App Itself&quot;">​</a></h2><p><code>mkbrut</code> created a lot of files for you, as well as command line apps to manage your app. We&#39;re going to perform app setup via <code>bin/setup</code>. This completely automates the following tasks:</p><ul><li>Installing RubyGems</li><li>Installing Node Modules</li><li>Installing Shopfiy&#39;s Ruby LSP, and Microsoft&#39;s JS and CSS LSPs</li><li>Creating your dev and test databases</li><li>Setting up Chromium, which we&#39;ll use to run end-to-end tests</li></ul><p>Run it now (remember, this is inside the container, so you should&#39;ve run <code>dx/exec bash</code> on your computer first)</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/setup</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
70
- <span class="line"><span>[ bin/setup ] Installing gems</span></span>
71
- <span class="line"><span>[ bin/setup ] Executing [&quot;bundle check --no-color || bundle install --no-color --quiet&quot;]</span></span>
72
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
73
- <span class="line"><span>[ bin/setup ] All set up.</span></span>
74
- <span class="line"><span></span></span>
75
- <span class="line"><span>USEFUL COMMANDS</span></span>
76
- <span class="line"><span></span></span>
77
- <span class="line"><span> bin/dev</span></span>
78
- <span class="line"><span> # run app locally, rebuilding and reloading as needed</span></span>
79
- <span class="line"><span></span></span>
80
- <span class="line"><span> bin/ci</span></span>
81
- <span class="line"><span> # runs all tests and checks as CI would</span></span>
82
- <span class="line"><span></span></span>
83
- <span class="line"><span> bin/console</span></span>
84
- <span class="line"><span> # get an IRB console with the app loaded</span></span>
85
- <span class="line"><span></span></span>
86
- <span class="line"><span> bin/db</span></span>
87
- <span class="line"><span> # interact with the DB for migrations, information, etc</span></span>
88
- <span class="line"><span></span></span>
89
- <span class="line"><span> bin/dbconsole</span></span>
90
- <span class="line"><span> # get a PSQL session to the database</span></span>
91
- <span class="line"><span></span></span>
92
- <span class="line"><span> bin/scaffold</span></span>
93
- <span class="line"><span> # Create various structures in your app, like pages or forms</span></span>
94
- <span class="line"><span></span></span>
95
- <span class="line"><span> bin/setup help</span></span>
96
- <span class="line"><span> # show this help</span></span></code></pre></div><p>When this is done, we can check that everything&#39;s working by running <code>bin/ci</code>. <code>bin/ci</code> runs all tests and quality checks. Even though you haven&#39;t written any code, there&#39;s just a bit included to ensure that what little is there is working properly. It&#39;s a good check before you start to make sure install and setup worked.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/ci</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
97
- <span class="line"><span>[ bin/ci ] Building Assets</span></span>
98
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
99
- <span class="line"><span>[ bin/ci ] Running non E2E tests</span></span>
100
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
101
- <span class="line"><span>[ bin/ci ] Running JS tests</span></span>
102
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
103
- <span class="line"><span>[ bin/ci ] Running E2E tests</span></span>
104
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
105
- <span class="line"><span>[ bin/ci ] Analyzing Ruby gems for</span></span>
106
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
107
- <span class="line"><span>[ bin/ci ] security vulnerabilities</span></span>
108
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
109
- <span class="line"><span>[ bin/ci ] Checking to see that all classes have tests</span></span>
110
- <span class="line"><span>«LOTS OF OUTPUT»</span></span>
111
- <span class="line"><span>[ bin/ci ] Done</span></span></code></pre></div><p>Finally, we&#39;ll run the app itself via <code>bin/dev</code></p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/dev</span></span></code></pre></div><p><code>bin/dev</code> won&#39;t exit, it&#39;ll sit there running your app until you hit <code>Ctrl-C</code>. Amongst the output you should see a message like:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
112
- <span class="line"><span>« LOTS OF OUTPUT »</span></span>
113
- <span class="line"><span>20:43:41 startup_message.1 | Your app is now running at</span></span>
114
- <span class="line"><span>20:43:41 startup_message.1 | </span></span>
115
- <span class="line"><span>20:43:41 startup_message.1 | http://localhost:6502</span></span>
116
- <span class="line"><span>20:43:41 startup_message.1 |</span></span></code></pre></div><p>Go to <a href="http://localhost:6502" target="_blank" rel="noreferrer">http://localhost:6502</a> in your web browser. You should see a welcome screen like so:</p><p><img src="`+t+`" alt="Screenshot of the Brut welcome screen"></p><h2 id="the-blog-we-ll-build" tabindex="-1">The Blog We&#39;ll Build <a class="header-anchor" href="#the-blog-we-ll-build" aria-label="Permalink to &quot;The Blog We&#39;ll Build&quot;">​</a></h2><p>We&#39;re ready to write some code! Here&#39;s how the blog is going to work:</p><ul><li>A blog post has a title and content, with each paragraph of the content separated with <code>\\n\\r</code>, which is what the browser inserts when you hit return.</li><li>The home page will show all the blog posts in reverse chronological order.</li><li>The home page will link to the edit blog post page where a blog post can be created.</li><li>Blog posts will be submitted to the backend to be saved, with the following constraints: <ul><li>title and content are required</li><li>title must be at least three characters</li><li>content must be at least 5 words (i.e. space-separated tokens)</li></ul></li></ul><p>We&#39;ll discuss tests later. To make it easier to follow Brut, we&#39;ll get things working first and then test them. You can absolutely do TDD with Brut, but we find it&#39;s hard to learn new things this way.</p><p>Let&#39;s start not from the database, but from the user experience.</p><h2 id="building-and-styling-pages" tabindex="-1">Building and Styling Pages <a class="header-anchor" href="#building-and-styling-pages" aria-label="Permalink to &quot;Building and Styling Pages&quot;">​</a></h2><p>The home page of a Brut app is served, naturally, on <code>/</code> and is implemented by the class <code>HomePage</code>, located in <code>app/src/front_end/pages/home_page.rb</code>.</p><p>A <em>page</em> in Brut is a Phlex component that is rendered inside a layout. A layout is common markup that all pages should have, such as the <code>&lt;head&gt;</code> section and perhaps a <code>&lt;body&gt;</code> or other tags. <code>mkbrut</code> provided a default layout that&#39;s good for now, so we just have to worry about the HTML that is specific to a page.</p><p>Open up <code>app/src/front_end/pages/home_page.rb</code> in your editor. You should see something like this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
117
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
118
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # The duplication and excessive class sizes here are to</span></span>
119
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # make it easier for you to remove this when you start working</span></span>
120
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # on your app. There are pros and cons to how this code</span></span>
121
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # is written, so don&#39;t take this is as a directive on how to</span></span>
122
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # build your app. You do you!</span></span>
123
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> img</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">src:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;/static/images/LogoPylon.png&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
124
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;dn db-ns pos-fixed top-0 left-0 h-100vh w-auto z-2&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
125
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> </span></span>
126
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> header</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;flex flex-column items-center justify-center h-100vh&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
127
- <span class="line"></span>
128
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # A lot more code</span></span>
129
- <span class="line"></span>
130
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
131
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
132
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><code>page_template</code> is where you can call Phlex to generate HTML.</p><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Phlex components use <code>view_template</code>, and that&#39;s what components in Brut use, too. Pages, however, use <code>page_template</code> so that the HTML can be placed inside a layout. <code>page_template</code> is a Brut concept, not a Phlex one.</p></div><h3 id="creating-the-homepage" tabindex="-1">Creating the HomePage <a class="header-anchor" href="#creating-the-homepage" aria-label="Permalink to &quot;Creating the HomePage&quot;">​</a></h3><p>Delete all the code in <code>page_template</code> and replace it with this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
133
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> header </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
134
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h1 { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My Amazing Blog&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
135
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Write New Blog Post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
136
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
137
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> main </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
138
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Posts go here&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
139
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
140
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If you&#39;ve never used Phlex before, it&#39;s a Ruby API that defines one method for each HTML element (along with any custom elements you tell it about). You then call these methods to build up markup. As you can see, it&#39;s structurally identical to HTML, but it&#39;s Ruby.</p><p>If your server is still running, refresh the page and you&#39;ll see this wonderful UI (otherwise, start your server with <code>bin/dev</code>):</p><p><img src="`+p+`" alt="Screenshot of the page we built"></p><p>Let&#39;s make it a bit nicer.</p><h3 id="using-css" tabindex="-1">Using CSS <a class="header-anchor" href="#using-css" aria-label="Permalink to &quot;Using CSS&quot;">​</a></h3><p>Open up <code>app/src/front_end/css/index.css</code> in your editor. You should see this:</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">@import</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;brut-css/dist/brut.css&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
141
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">@import</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;svgs.css&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span></code></pre></div><p>Brut uses esbuild to bundle CSS. esbuild makes use of the standard <code>@import</code> directive. All <code>@imports</code> are relative to the current file or to <code>node_modules</code>. <code>brut-css/dist/brut.css</code> is the BrutCSS library that comes with Brut. We aren&#39;t going to use it, just to keep things focused. <code>svgs.css</code> is located in <code>app/src/front_end/css/svgs.css</code> and sets up a few classes for inline SVGs.</p><p>We&#39;ll add some CSS for the home page right here. We&#39;ll use vanilla CSS to avoid going on a deep dive on CSS frameworks.</p><p>Add this below <code>@import &quot;svgs.css&quot;;</code></p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
142
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">50</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
143
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> margin-left</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">auto</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
144
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> margin-right</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">auto</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
145
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span>
146
- <span class="line"></span>
147
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">header</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
148
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border-bottom</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> thin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> gray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
149
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">flex</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
150
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> align-items</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">baseline</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
151
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> justify-content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">space-between</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
152
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
153
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> gap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">0.5</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
154
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>If you reload the home page in your browser, it now looks at least a little bit respectible:</p><p><img src="`+l+`" alt="Screenshot of the styled home page"></p><p>Now, let&#39;s build the blog post editor.</p><h2 id="creating-forms" tabindex="-1">Creating Forms <a class="header-anchor" href="#creating-forms" aria-label="Permalink to &quot;Creating Forms&quot;">​</a></h2><p>To create blog posts, we need three things:</p><ul><li>A page where the creation happens, which will host an HTML <code>&lt;form&gt;</code></li><li>A URL where that <code>&lt;form&gt;</code> will be submitted</li><li>Some code to handle the submissions</li></ul><h3 id="creating-a-new-page" tabindex="-1">Creating a New Page <a class="header-anchor" href="#creating-a-new-page" aria-label="Permalink to &quot;Creating a New Page&quot;">​</a></h3><p>To make a new page in Brut, we&#39;ll need to declare a route, and Brut will choose the class name. We&#39;ll use <code>/blog_post_editor</code>, meaning Brut will expect <code>BlogPostEditorPage</code> to exist. We can do all this at once with <code>bin/scaffold page</code>. <code>bin/scaffold page</code> accepts the URL of the page we want to build. Brut will use that URL to figure out the page class&#39; name and generate it, along with a failing test. It will also insert the route into <code>app.rb</code>. Run it now, like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/scaffold</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> page</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> /blog_post_editor</span></span></code></pre></div><p>Your output should look like so:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
155
- <span class="line"><span>[ bin/scaffold ] Inserted route into app/src/app.rb</span></span>
156
- <span class="line"><span>[ bin/scaffold ] Page source is in app/src/front_end/pages/blog_post_editor_page.rb</span></span>
157
- <span class="line"><span>[ bin/scaffold ] Page test is in specs/front_end/pages/blog_post_editor_page.spec.rb</span></span>
158
- <span class="line"><span>[ bin/scaffold ] Added title to app/config/i18n/en/2_app.rb</span></span>
159
- <span class="line"><span>[ bin/scaffold ] Added route to app/src/app.rb</span></span></code></pre></div><p>Restart your server (Brut currently cannot auto-reload new routes).</p><p>If you manually navigate to <code>http://localhost:6502/blog_post_editor</code>, you can see a very basic page has been created. Before we build the actual page, let&#39;s change the home page so it links here.</p><p>If you&#39;ll recall, we had a <code>a(href:&quot;&quot;) { ... }</code> in our template. We now know the URL for that <code>href</code>. We <em>could</em> use the actual url, <code>/blog_post_editor</code>, but it&#39;s going to be easier to manage our app over time if we don&#39;t hard-code paths and instead use our new page class to generate the URL.</p><p>Open up <code>app/src/front_end/pages/home_page.rb</code> and make this change:</p><div class="language-diff vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">diff</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#B31D28;--shiki-dark:#FDAEB7;">- a(href: &quot;&quot;) { &quot;Write New Blog Post&quot; }</span></span>
160
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">+ a(href: BlogPostEditorPage.routing) { &quot;Write New Blog Post&quot; }</span></span></code></pre></div><p>All page classes have a <code>.routing</code> method. By using this instead of building a URL ourselves, we get some advantages:</p><ul><li>If we rename or remove <code>BlogPostEditorPage</code>, any page referencing it will generate a nice, easy-to-understand error.</li><li><code>routing</code> can manage query strings and anchors in a safe way, plus it can check that if a page has required routing parameters (e.g. the <code>123</code> in <code>/posts/123</code>), that they are provided.</li></ul><p>With this change, you can now click the link and see the <code>BlogPostEditorPage</code>&#39;s template we saw earlier.</p><p>Most of the <code>BlogPostEditorPage</code>&#39;s HTML will be a form to submit a new blog post. While we could write that using HTML, Brut makes life easier by allowing the use of a <em>form class</em> to do it, which also will describe the data to be submitted to the server. This data is handled by a <em>handler</em>.</p><h3 id="create-a-form-and-handler" tabindex="-1">Create a Form and Handler <a class="header-anchor" href="#create-a-form-and-handler" aria-label="Permalink to &quot;Create a Form and Handler&quot;">​</a></h3><p>A form gets submitted to a URL, and Brut routes that submission to a handler. To create both a form class and a handler, we&#39;ll use <code>bin/scaffold form</code>, giving it the URL to respond on.</p><p>In this case, we&#39;ll use the URL <code>/new_blog_post</code>. Stop your server and run this command:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/scaffold</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> form</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> /new_blog_post</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
161
- <span class="line"><span>[ bin/scaffold ] NewBlogPostForm in app/src/front_end/forms/new_blog_post_form.rb</span></span>
162
- <span class="line"><span>[ bin/scaffold ] NewBlogPostHandler in app/src/front_end/handlers/new_blog_post_handler.rb</span></span>
163
- <span class="line"><span>[ bin/scaffold ] Spec in specs/front_end/handlers/new_blog_post_handler.spec.rb</span></span>
164
- <span class="line"><span>[ bin/scaffold ] Inserted route into app/src/app.rb</span></span></code></pre></div><p>When creating a new form, the first thing we have to do is edit the form class (in this case, <code>NewBlogPostForm</code>, located in <code>app/src/front_end/forms/new_blog_post_form.rb</code>) to describe the values being accepted by the server.</p><p>This can be done by calling static/class methods provided by <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a>, the superclass of <code>AppForm</code>, which is the superclass of our app&#39;s forms.</p><p>Open up <code>app/src/front_end/forms/new_blog_post_form.rb</code>. We&#39;ll call <code>input</code> twice, once for the title and once for the content. <code>input</code> takes keyword arguments that mirror those of the web platform&#39;s constraint validation system. Since our title must be at least 3 characters, that means we&#39;ll use <code>minlength</code> to specify this.</p><p>Here&#39;s the code:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
165
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">minlength:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 3</span></span>
166
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span></span>
167
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Each field is required by default (you can set <code>required: false</code> on fields that aren&#39;t required).</p><p>With these declarations, we can use an instance of this class to generate HTML.</p><h3 id="generating-an-html-form" tabindex="-1">Generating an HTML Form <a class="header-anchor" href="#generating-an-html-form" aria-label="Permalink to &quot;Generating an HTML Form&quot;">​</a></h3><p>The <code>BlogPostEditorPage</code> will contain the form used to write a blog post. This page must make sure two things happen:</p><ul><li>When someone navigates to it, it should show the form with nothing in the fields.</li><li>When there is an error in what the blog post author has provided, it should show those errors, but also maintain the inputs the author provided.</li></ul><p>To do this, the <code>BlogPostEditorPage</code> will need an instance of <code>NewBlogPostForm</code>. We can create this in its constructor. Open up <code>app/src/front_end/pages/blog_post_editor_page.rb</code> and start it off like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
168
- <span class="line"></span>
169
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
170
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
171
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
172
- <span class="line"></span>
173
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
174
- <span class="line"></span>
175
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Next, we&#39;ll implement <code>page_template</code> and we&#39;ll use <code>@form</code> to create HTML for the form&#39;s inputs, including client-side constraints and, as we&#39;ll see later, pre-existing values from a previous submission.</p><p>This will require four parts of Brut&#39;s API and use one optional one:</p><ul><li><code>brut_form</code>, a custom element (<a href="/brut-js/api/Form.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-form&gt;</code></a>) that will progressively enhance the form to provide constraint violation visitor experience if JavaScript is enabled.</li><li><code>FormTag</code>, a Phlex component provided by Brut that generates the correct <code>&lt;form&gt;</code> element, as well as CSRF protection.</li><li><code>Inputs::</code> components, namely <a href="/api/Brut/FrontEnd/Components/Inputs/InputTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::InputTag</code></a> and <a href="/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::TextareaTag</code></a>, which generate <code>&lt;input&gt;</code> and <code>&lt;textarea&gt;</code>, respectively. These Phlex components (provided by Brut) will add the correct attributes for validation, and set the values if the form they are given has values set.</li><li><a href="/api/Brut/FrontEnd/Components/ConstraintViolations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::ConstraintViolations</code></a>, a Phlex component provided by Brut that generates custom elements that, when JavaScript is enabled, allow for control over the visitor experience when there are constraint violations.</li><li><em>(optional)</em> <code>t</code> provides access to localized strings, instead of hard-coding English.</li></ul><p>Create <code>page_template</code> to look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
176
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h1 { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:write_new_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) }</span></span>
177
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> brut_form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
178
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FormTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">for:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
179
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> label </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
180
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">InputTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">autofocus:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
181
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> div { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">([</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]) }</span></span>
182
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
183
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
184
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> label </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
185
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextareaTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">rows:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
186
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> div { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">([</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] ) }</span></span>
187
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
188
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
189
- <span class="line"></span>
190
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">([</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]) }</span></span>
191
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
192
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
193
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>You&#39;ll notice that we mentioned classes like <a href="/api/Brut/FrontEnd/Components/Inputs/InputTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::InputTag</code></a>, but the code above is only using <code>Input::InputTag</code>. This is due to <a href="https://www.phlex.fun/components/kits.html" target="_blank" rel="noreferrer"><em>Phlex Kits</em></a>, which allow you to use relative class names in certain circumstances.</p><p>Brut makes use of this so there is a clear and organized name for a component, but you almost never have to type or read the entire thing.</p></div><p>Make sure your server is running, then reload the blog post editor page. You should see an error like so:</p><blockquote><p><code>Translation missing. Options considered were: - en.pages.BlogPostEditorPage.write_new_post - en.write_new_post</code></p></blockquote><p>Let&#39;s add those keys.</p><h3 id="adding-translation-keys" tabindex="-1">Adding Translation Keys <a class="header-anchor" href="#adding-translation-keys" aria-label="Permalink to &quot;Adding Translation Keys&quot;">​</a></h3><p>In Brut, translations aren&#39;t stored in YAML 🥳🎉, but in a Ruby hash. Brut provides standard translations in <code>app/config/i18n/en/1_defaults.rb</code>, but your app will set its own in <code>app/config/i18n/en/2_app.rb</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># All app-specific translations, or overrides of Brut&#39;s defaults, go here.</span></span>
194
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">{</span></span>
195
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # en: must be the first entry, thus indicating this is the EN translations</span></span>
196
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> en:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
197
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cv:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
198
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cs:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
199
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
200
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ss:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
201
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email_taken:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;This email has been taken&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
202
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
203
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
204
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> pages:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Page-specific messages - this key is relied-upon by Brut to exist</span></span>
205
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
206
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Welcome!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
207
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
208
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
209
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;BlogPostEditorPage&quot;</span></span>
210
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
211
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
212
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ... more code</span></span></code></pre></div><p>When you use <code>t</code> on a page in Brut, it looks for <code>pages.«page name».«key»</code>, so Brut needs from our form:</p><ul><li><code>pages.BlogPostEditorPage.write_new_post</code></li><li><code>pages.BlogPostEditorPage.form.title</code></li><li><code>pages.BlogPostEditorPage.form.content</code></li><li><code>pages.BlogPostEditorPage.form.post</code></li></ul><p>Give them values like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># All app-specific translations, or overrides of Brut&#39;s defaults, go here.</span></span>
213
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">{</span></span>
214
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # en: must be the first entry, thus indicating this is the EN translations</span></span>
215
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> en:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
216
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cv:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
217
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cs:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
218
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
219
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ss:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
220
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email_taken:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;This email has been taken&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
221
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
222
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
223
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> pages:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Page-specific messages - this key is relied-upon by Brut to exist</span></span>
224
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
225
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Welcome!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
226
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
227
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
228
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;BlogPostEditorPage&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
229
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> write_new_post:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Write a new post!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
230
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
231
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Title&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
232
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> content:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Post Content&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
233
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> post:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Post It!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
234
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
235
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
236
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
237
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ... more code</span></span></code></pre></div><p>Now, when you reload the page, it should work:</p><p><img src="`+h+'" alt="screenshot of the form working, but unstyled"></p><p>Without filling anything in, click the submit button. The form should show you some error messages that are unstyled:</p><p><img src="'+o+`" alt="screenshot of the form working, but unstyled"></p><p>Let&#39;s style them and learn about how the <a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> tags created by <code>ConstraintViolations</code> work.</p><h3 id="styling-constraint-violations" tabindex="-1">Styling Constraint Violations <a class="header-anchor" href="#styling-constraint-violations" aria-label="Permalink to &quot;Styling Constraint Violations&quot;">​</a></h3><p>If you view source, you should see HTML like so:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&#39;title&#39;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
238
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>If you click submit and view source, you&#39;ll see something like this:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&#39;title&#39;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
239
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;This field is required&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
240
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>This was inserted by <a href="/brut-js/api/Form.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-form&gt;</code></a> whenever an element of the form is invalid. This could happen before the visitor clicks &quot;submit&quot;, however. To allow you to style these elements only when a submit has been attempted, <a href="/brut-js/api/Form.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-form&gt;</code></a> will set the attribute <code>submitted-invalid</code> on itself when this happens.</p><p>Open <code>app/src/front_end/css/index.css</code> in your editor. We want <a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> messages to be red, bold, and in the body font size. We also want them hidden by default.</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
241
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">none</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
242
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">#A60053</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
243
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-weight</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">bold</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
244
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
245
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>When <code>submitted-invalid</code> is set on <code>brut-form</code>, <em>then</em> we show them. We <em>also</em> want to show them if they were generated from the server, which <code>ConstraintViolations</code> will do:</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">submitted-invalid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
246
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server-side</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] {</span></span>
247
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">block</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
248
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>Let&#39;s also do some styling for the form and its elements. Add this below the CSS you just wrote:</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">.BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
249
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> brut-form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
250
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">block</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
251
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
252
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> thin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> gray</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
253
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border-radius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">0.25</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
254
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">#eeeeee</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
255
- <span class="line"></span>
256
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
257
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">flex</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
258
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> flex-direction</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">column</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
259
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> gap</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
260
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> align-items</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">start</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
261
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
262
- <span class="line"></span>
263
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">textarea</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
264
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
265
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">0.5</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
266
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">130</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
267
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
268
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> label</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
269
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> width</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">100</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
270
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">120</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
271
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">block</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
272
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
273
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-weight</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">bold</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
274
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-style</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">italic</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
275
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
276
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
277
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;"> button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
278
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding-left</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
279
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding-right</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">2</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
280
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding-top</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
281
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> padding-bottom</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
282
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">#E5FFE5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
283
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> thin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> #006300</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
284
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">#006300</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
285
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border-radius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
286
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> font-size</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">150</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">%</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
287
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> align-self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">end</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
288
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cursor</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">pointer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
289
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &amp;:hover {</span></span>
290
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> background-color: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">#ACFFAC</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
291
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
292
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
293
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
294
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>Two notes about this CSS:</p><ul><li><p>It&#39;s using nesting, which is part of <a href="https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility" target="_blank" rel="noreferrer">Baseline</a></p></li><li><p>We&#39;ve nested all the CSS inside the <code>.BlogPostEditorPage</code> class. The default layout Brut provides includes this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @page_name) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
295
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> yield</span></span>
296
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This means all pages have their page name set on the <code>&lt;body&gt;</code> tag, allowing nested CSS, if you like.</p></li></ul><p><em>Now</em>, if you submit the form without providing any values, you&#39;ll see a decent-looking experience:</p><p><img src="`+k+`" alt="screenshot of the styled form with constraint violations"></p><p>If you fill out the fields correctly, you should see an error that you need to implement your handler. Let&#39;s do that next.</p><h2 id="handling-form-submissions" tabindex="-1">Handling Form Submissions <a class="header-anchor" href="#handling-form-submissions" aria-label="Permalink to &quot;Handling Form Submissions&quot;">​</a></h2><p>When you ran <code>bin/scaffold form</code> earlier, it created <code>NewBlogPostHandler</code>. It&#39;s located in <code>app/src/front_end/handlers/new_blog_post_handler.rb</code>, which should look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
297
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
298
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
299
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
300
- <span class="line"></span>
301
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
302
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> raise</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;You need to implement your handler&quot;</span></span>
303
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
304
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The <code>handle</code> method is expected to return a value that tells Brut how to respond to a form submission. In our case, we&#39;ll either want it to re-generate <code>BlogPostEditorPage</code>&#39;s HTML with error messages and the visitor-supplied form fields pre-filled in, or save the blog post and redirect back to <code>HomePage</code>.</p><p>To do that, we&#39;ll either return an instance of <code>BlogPostEditorPage</code>, or return a <code>URI</code> to <code>HomePage</code> (which we can do with the helper method <code>redirect_to</code>).</p><p>Before <code>handle</code> is called, <code>NewBlogPostHandler</code> will be initialized and give an instance of <code>NewBlogPostForm</code> containing whatever data was submitted by the browser. <code>handle</code> can then use <code>@form</code> to determine what to do.</p><p>First, we&#39;ll re-check client-side validations by calling <code>.valid?</code>. If that&#39;s true, we can perform server-side validations, calling <code>server_side_constraint_violation</code> for any violations we find. Then, we&#39;ll check <code>.valid?</code> again and return a <code>BlogPostEditorPage</code> or redirect.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
305
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
306
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
307
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
308
- <span class="line"></span>
309
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
310
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">valid?</span></span>
311
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\s</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span></span>
312
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
313
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
314
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :not_enough_words</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
315
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">num_words:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
316
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
317
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
318
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
319
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">valid?</span></span>
320
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # TODO: Actually save the post</span></span>
321
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
322
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
323
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
324
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
325
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
326
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Of course, <code>BlogPostEditorPage</code> does not accept the form as a parameter. We&#39;ll need to change that. Since we are using the <code>@form</code> instance to help generate HTML, if we pass the instance from our handler to the <code>BlogPostEditorPage</code>, when <em>that</em> instance generates HTML, it will have errors indicated and show the visitor&#39;s provided values instead of defaults.</p><p>Of course, we still need to create a blank form when the page is accessed for the first time, so we&#39;ll make <code>form:</code> default to <code>nil</code> and create it if we aren&#39;t given a value:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
327
- <span class="line highlighted"><wbr></span>
328
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
329
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
330
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span></code></pre></div><p>With this in place, create a new blog post but with only four words in the content. This will pass client-side checks, but fail server-side checks. When you submit, you&#39;ll see an error related to <code>cv.ss.not_enough_words</code>, which is the key Brut is trying to use to find the actual error message.</p><blockquote><p><code>Translation missing. Options considered were: - en.components.Brut::FrontEnd::Components::ConstraintViolations.cv.ss.not_enough_words - en.cv.ss.not_enough_words</code></p></blockquote><p>Add it to <code>app/config/i18n/en/2_app.rb</code>, under <code>en</code>, <code>cv</code> (for constraint violations), <code>ss</code> (for server-side):</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># All app-specific translations, or overrides of Brut&#39;s defaults, go here.</span></span>
331
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">{</span></span>
332
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # en: must be the first entry, thus indicating this is the EN translations</span></span>
333
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> en:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
334
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cv:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
335
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cs:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
336
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
337
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ss:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
338
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email_taken:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;This email has been taken&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
339
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> not_enough_words:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;%{field} does not have enough words. Must have %{num_words}&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
340
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
341
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span></code></pre></div><p><em>Now</em>, try again, and you&#39;ll see this message, rendered exactly like client-side errors:</p><p><img src="`+d+`" alt="screenshot of the styled form with server-generated constraint violations"></p><p>Now that we have the user experience in place, let&#39;s actually store the blog post in the database.</p><h2 id="using-a-database" tabindex="-1">Using a Database <a class="header-anchor" href="#using-a-database" aria-label="Permalink to &quot;Using a Database&quot;">​</a></h2><p>Brut uses Postgres, and includes and configures the <a href="https://sequel.jeremyevans.net/" target="_blank" rel="noreferrer">Sequel</a> library to access your data. Sequel has some similarity to Rails&#39; Active Record, but it&#39;s not quite the same.</p><p>The main way to access data is to create a <em>database model</em> class (which is similar to an Active Record). Sequel also provides a way to manage your database schema via <em>migrations</em>.</p><p>The steps to take when creating a new table you want to access are:</p><ol><li>Create a migration that creates the schema for the new table.</li><li>Create the database model class itself.</li><li>Create a FactoryBot factory that can create sample instances of rows in the table, useful for testing and development</li><li>Modify seed data to create sample data for development.</li></ol><p>Most of this can be done via <code>bin/scaffold db_model</code>.</p><h3 id="creating-a-new-database-table" tabindex="-1">Creating a New Database Table <a class="header-anchor" href="#creating-a-new-database-table" aria-label="Permalink to &quot;Creating a New Database Table&quot;">​</a></h3><p>Stop your server and run <code>bin/scaffold</code> like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/scaffold</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> db_model</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> blog_post</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
342
- <span class="line"><span>[ bin/scaffold ] Executing [&quot;bin/db new_migration create_blog_post&quot;]</span></span>
343
- <span class="line"><span>[ bin/db ] Migration created:</span></span>
344
- <span class="line"><span> app/src/back_end/data_models/migrations/20250811213758_create_blog_post.rb</span></span>
345
- <span class="line"><span>[ bin/scaffold ] [&quot;bin/db new_migration create_blog_post&quot;] succeeded</span></span>
346
- <span class="line"><span>[ bin/scaffold ] Creating DB::BlogPost in app/src/back_end/data_models/db/blog_post.rb</span></span>
347
- <span class="line"><span>[ bin/scaffold ] Creating spec for DB::BlogPost in specs/back_end/data_models/db/blog_post.spec.rb</span></span>
348
- <span class="line"><span>[ bin/scaffold ] Creating factory for DB::BlogPost in specs/factories/db/blog_post.factory.rb</span></span></code></pre></div><p>Your migration file name will be different than ours, since it has a timestamp embedded into it.</p><p>Open that file in your editor and use <code>create_table</code>, as provided by Sequel, to describe the table.</p><p>Brut enhances Sequel&#39;s <code>create_table</code> in the following ways:</p><ul><li>A numeric primary key called <code>id</code> is created.</li><li><code>comment:</code> is required.</li><li><code>external_id</code> can be given, and will create a managed unique key called <code>external_id</code> that is safe to externalize and is not used in foreign key or referential integrity.</li><li>A timestamped field, <code>created_at</code> is created and will be set when new rows are created from your app.</li></ul><p>Inside <code>create_table</code>, we&#39;ll call <code>column</code> to define columns. Brut defaults all columns to <code>NOT NULL</code>, so you don&#39;t need to specify <code>null: false</code>.</p><p>All of this goes inside a block given to the method <code>up</code>, like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">migration</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
349
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
350
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_posts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
351
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;All the posts fit to post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
352
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> external_id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
353
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
354
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
355
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
356
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
357
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If you&#39;ve used migrations before, you may know that <code>down</code> can be used to specify a way to undo the migration, or that a method like <code>change</code> can be used to automatically do both. Brut recommends only using forward migrations inside <code>up</code>. If you need to undo and redo your changes, you can use <code>bin/db rebuild</code> to rebuild your database from scratch.</p><p>Save this file, then apply this migration to your development database:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/db</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> migrate</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
358
- <span class="line"><span>[ bin/db ] Migrations applied</span></span></code></pre></div><p>Now, apply it to your test database:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/db</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> migrate</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -e</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> test</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
359
- <span class="line"><span>[ bin/db ] Migrations applied</span></span></code></pre></div><p>You can examine the table that was created by running <code>bin/dbconsole</code>:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/dbconsole</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
360
- <span class="line"><span>psql (16.9 (Debian 16.9-1.pgdg120+1), server 16.4 (Debian 16.4-1.pgdg120+2))</span></span>
361
- <span class="line"><span>Type &quot;help&quot; for help.</span></span>
362
- <span class="line"><span></span></span>
363
- <span class="line"><span>blog_development=#</span></span></code></pre></div><p>This will give you a new prompt where you can type commands to <code>psql</code>, the Postgres command-line client. Try describing the table:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">\\d</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> blog_posts</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span> Table &quot;public.blog_posts&quot;</span></span>
364
- <span class="line"><span> Column | Type | Collation | Nullable | Default </span></span>
365
- <span class="line"><span>-------------+--------------------------+-----------+----------+----------------------------------</span></span>
366
- <span class="line"><span> id | integer | | not null | generated by default as identity</span></span>
367
- <span class="line"><span> title | text | | not null | </span></span>
368
- <span class="line"><span> content | text | | not null | </span></span>
369
- <span class="line"><span> created_at | timestamp with time zone | | not null | </span></span>
370
- <span class="line"><span> external_id | citext | | not null | </span></span>
371
- <span class="line"><span>Indexes:</span></span>
372
- <span class="line"><span> &quot;blog_posts_pkey&quot; PRIMARY KEY, btree (id)</span></span>
373
- <span class="line"><span> &quot;blog_posts_external_id_key&quot; UNIQUE CONSTRAINT, btree (external_id)</span></span></code></pre></div><p><code>bin/scaffold</code> created the database model for you in <code>app/src/back_end/data_models/db/blog_post.rb</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppDataModel</span></span>
374
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> has_external_id </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:bl</span></span>
375
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>In Brut, database models are in the <code>DB::</code> namespace, so you do not conflate them with a <em>domain</em> model.</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>Note <code>has_external_id</code>. This tells Brut and Sequel that the underlying table is expected to have the field <code>external_id</code> and that it is expected to be unique. You can see this in the output of <code>\\d blog_posts</code>, where it says <code>UNIQUE CONSTRAINT, btree (external_id)</code>.</p><p><code>has_external_id</code> configures the database model to provide a value for this key when saving or creating a row. To aid in understanding the values out of context, external ids are prefixed with two values: one is an app-wide value, in our case <code>bl</code>. The other is a model-specific value, also <code>bl</code>. Thus, external ids might look like <code>blbl_9783245789345789345789345</code>.</p></div><p>Before we use this database model, we want to be able to create instances in tests, as well as for local development. The way to do that in Brut is to create a factory.</p><h3 id="creating-test-and-development-data" tabindex="-1">Creating Test and Development Data <a class="header-anchor" href="#creating-test-and-development-data" aria-label="Permalink to &quot;Creating Test and Development Data&quot;">​</a></h3><p>Brut uses <a href="https://github.com/thoughtbot/factory_bot" target="_blank" rel="noreferrer">FactoryBot</a> to create sample instance of your data. Open up <code>specs/factories/db/blog_post.factory.rb</code> in your editor.</p><p>If you are new to FactoryBot, it is a lightweight (ish) DSL that allows creating test data. You&#39;ll call methods based on the column names in order to specify values. Brut also includes <a href="https://github.com/faker-ruby/faker" target="_blank" rel="noreferrer">Faker</a>, which creates randomized but realistic test data.</p><p>For the title, we&#39;ll use Faker&#39;s &quot;hipster ipsum&quot;. For the content, we want several paragraphs delineated by <code>\\n\\r</code>, so we&#39;ll create between 2 and 6 paragraphs and join them.</p><p>Make <code>specs/factories/db/blog_post.factory.rb</code> look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">define</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
376
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> factory </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;DB::BlogPost&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
377
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Faker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Hipster</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">sentence</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
378
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> content {</span></span>
379
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> (</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">rand</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">+</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">times</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
380
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Faker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Hipster</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">paragraph_by_chars</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">characters:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 200</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
381
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">\\n\\r</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
382
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
383
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
384
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Brut includes a test to make sure your factories are valid and will work. It&#39;s in <code>specs/lint_factories.spec.rb</code>. Run it now to make sure this factory works:</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> specs/lint_factories.spec.rb</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
385
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/lint_factories.spec.rb\\&quot;&quot;]</span></span>
386
- <span class="line"><span>Run options: exclude {e2e: true}</span></span>
387
- <span class="line"><span></span></span>
388
- <span class="line"><span>Randomized with seed 29315</span></span>
389
- <span class="line"><span></span></span>
390
- <span class="line"><span>factories</span></span>
391
- <span class="line"><span> should be possible to create them all</span></span>
392
- <span class="line"><span></span></span>
393
- <span class="line"><span>Finished in 0.59465 seconds (files took 0.7718 seconds to load)</span></span>
394
- <span class="line"><span>1 example, 0 failures</span></span>
395
- <span class="line"><span></span></span>
396
- <span class="line"><span>Randomized with seed 29315</span></span>
397
- <span class="line"><span></span></span>
398
- <span class="line"><span>[ bin/test ] [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/lint_factories.spec.rb\\&quot;&quot;] succeeded</span></span></code></pre></div><p>We can use this factory for seed data to provide realistic data for development. Edit <code>app/src/back_end/data_models/seed/seed_data.rb</code>, and make it look like so, which will create 10 blog posts:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;brut/back_end/seed_data&quot;</span></span>
399
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SeedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BackEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">SeedData</span></span>
400
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Syntax</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
401
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> seed!</span></span>
402
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">times</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |i|</span></span>
403
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">created_at:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Date</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">today</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> -</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> i)</span></span>
404
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
405
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
406
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><code>create</code> is a method provided by Factory Bot that is brought in via <code>FactoryBot::Syntax::Methods</code>.</p><p>Now, load the seed data into the development database with <code>bin/db seed</code>:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/db</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> seed</span></span></code></pre></div><p>We can now show this data on the home page.</p><h2 id="accessing-the-database" tabindex="-1">Accessing the Database <a class="header-anchor" href="#accessing-the-database" aria-label="Permalink to &quot;Accessing the Database&quot;">​</a></h2><p>On <code>HomePage</code>, we put in a <code>&lt;p&gt;</code> as a placeholder for blog posts. Let&#39;s replace that with code to fetch and display the blog posts.</p><p>In Brut, since HTML is generated by Phlex and thus by Ruby code, we can structure our HTML generation however we like, including by accessing the database directly. This may not scale as our app gets large, but for now, it&#39;s the simplest thing to do.</p><p>Sequel&#39;s database models are similar to Rails&#39; Active Record&#39;s in that we can call class methods to access data. In this case, <code>DB::BlogPost</code> has a method <code>order</code> that will fetch all records sorted by the field we give it in the order we decide. The sort field and order is specified via <code>Sequel.desc</code> for descending or <code>Sequel.asc</code> for ascending. We want posts in reverse-chronological order, so <code>Sequel.desc(:created_at)</code> will achieve this.</p><p>We can call <code>.each</code> on the result and iterate over each blog post. For the content, we&#39;ll split by <code>\\n\\r</code> to create paragraphs.</p><p>Here&#39;s what <code>HomePage</code>&#39;s <code>page_template</code> should look like now:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
407
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> header </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
408
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h1 { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My Amazing Blog&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
409
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Write New Blog Post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
410
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
411
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> main </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
412
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">order</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">desc</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:created_at</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">each</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |blog_post|</span></span>
413
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> article </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
414
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h2 { blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
415
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\n\\r</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">each</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |paragraph|</span></span>
416
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { paragraph }</span></span>
417
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
418
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
419
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> hr</span></span>
420
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
421
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
422
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Start your server if you stopped it before. Go to the home page, and you should see our fake blog posts:</p><p><img src="`+r+`" alt="Home page showing two posts from the seed data, formatted properly"></p><p>If we modify our handler to save new posts to the database, they&#39;ll show up here.</p><h2 id="saving-to-the-database" tabindex="-1">Saving to the Database <a class="header-anchor" href="#saving-to-the-database" aria-label="Permalink to &quot;Saving to the Database&quot;">​</a></h2><p>To create rows in the database, the class method <code>create</code> can be called on <code>DB::BlogPost</code>. Let&#39;s change the handler to use that. Open up <code>app/src/front_end/handlers/new_blog_post_handler.rb</code> and make <code>handle</code> look like so (the changed lines are highlighted):</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
423
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">@form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
424
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\s</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span></span>
425
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
426
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
427
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :not_enough_words</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
428
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">num_words:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
429
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
430
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
431
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
432
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">valid?</span></span>
433
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
434
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
435
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> content:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span></span>
436
- <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
437
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
438
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
439
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
440
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
441
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The form object provides access to the values of any field we&#39;ve declared via a method call.</p><p>Now, create a new blog post, provide valid data, and submit it.</p><p><img src="`+c+'" alt="Screenshot of the blog post editor, with a new post filled in"></p><p>Once you submit it, you should see the home page with your new post at the top:</p><p><img src="'+g+`" alt="Screenshot of the home page, showing the new blog post"></p><p>Our work isn&#39;t quite done. We need tests.</p><h2 id="testing-brut-apps" tabindex="-1">Testing Brut Apps <a class="header-anchor" href="#testing-brut-apps" aria-label="Permalink to &quot;Testing Brut Apps&quot;">​</a></h2><p>We&#39;ll create the following tests:</p><ul><li>Check that the logic in the handler is sound</li><li>Check that blog posts show up on the home page</li><li>Check that the entire workflow of create a blog post and seeing it show up on the home page works in a real web browser</li></ul><p>Let&#39;s test our handler first, as that is where the main logic is.</p><h3 id="testing-handlers" tabindex="-1">Testing Handlers <a class="header-anchor" href="#testing-handlers" aria-label="Permalink to &quot;Testing Handlers&quot;">​</a></h3><p>Our handler will need three tests:</p><ul><li>If the form was submitted without client-side validations happening, we should not create a new blog post and re-generate the blog post editor page, showing the errors.</li><li>If client-side validations pass, but the blog post isn&#39;t five words or more, we should not create a new blog post and re-generate the blog post editor page, showing the errors.</li><li>If everything looks good, we save the new blog post and redirect to the home page.</li></ul><p>Brut apps are tested with RSpec, and Brut provides several convenience methods and matchers to make testing as painless as possible.</p><p>When testing a handler, the public method is <code>handle!</code>, not <code>handle</code>, so we want to call that (Brut implements <code>handle!</code> to call <code>handle</code>).</p><p>First, we&#39;ll test client-side validations. Open up <code>specs/front_end/handlers/new_blog_post_handler.spec.rb</code> and replace the code there with this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;spec_helper&quot;</span></span>
442
- <span class="line"></span>
443
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewBlogPostHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
444
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> describe </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;#handle!&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
445
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> context </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;client-side violations got to the server&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
446
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;re-generates the HTML for the BlogPostEditorPage&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
447
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">NewBlogPostForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">params:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {})</span></span>
448
- <span class="line"></span>
449
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">handle!</span></span>
450
- <span class="line"></span>
451
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_generated</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
452
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :valueMissing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
453
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :valueMissing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
454
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
455
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
456
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
457
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><code>have_generated</code> asserts that the value returned from <code>handle!</code> is an instance of the page given, <code>BlogPostEditorPage</code> in this case. You could just as easily write <code>expect(result.kind_of?(BlogPostEditorPage)).to eq(true)</code>, but <code>have_generated</code> expressed the intent of what&#39;s happening.</p><p><code>have_constraint_violation</code> checks that the form&#39;s <code>constraint_violations</code> contains one for the given field and the given key. In this case, we check both <code>:title</code> and <code>:content</code> for <code>:valueMissing</code>.</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>Client-side constraint violations use the same keys on the server as they do in the browser. In the case of a required field, the browser&#39;s <a href="https://developer.mozilla.org/en-US/docs/Web/API/ValidityState" target="_blank" rel="noreferrer"><code>ValidityState</code></a> would set <code>valueMissing</code> to true. So, that&#39;s what Brut would do on the server-side, when reflecting client-side constraints.</p></div><p>Next, we&#39;ll check that the server-side constraint violations are being checked. Add this just below the <code>context</code> you just added:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">context </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;post is not enough words&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
458
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;re-generates the HTML for the BlogPostEditorPage, with server-side errors indicated&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
459
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">NewBlogPostForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">params:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
460
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;What a great post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
461
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> content:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Not enough words&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
462
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
463
- <span class="line"></span>
464
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> confidence_check { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) }</span></span>
465
- <span class="line"></span>
466
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">handle!</span></span>
467
- <span class="line"></span>
468
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_generated</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
469
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :not_enough_words</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
470
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
471
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This test introduces two new concepts:</p><ul><li>To initialize a form with data, you must pass a <code>Hash</code> to the keyword argument <code>params:</code>. If the <code>Hash</code> contains parameters that the form doesn&#39;t recognize, they are ignored and discarded.</li><li>Although we aren&#39;t expecting the form to have client-side constraint violations, if there are any, the test would fail in a confusing way. To manage this, Brut includes the <a href="https://github.com/sustainable-rails/confidence-check" target="_blank" rel="noreferrer">confidence-check</a> gem that allows you to make assertions that are not part of the test. If the confidence check fails, the test output will be obvious that the test could not run due to an assumption being violated.</li></ul><p>Lastly, we&#39;ll check that everything worked when there aren&#39;t any constraint violations. Add this below the <code>context</code> you just added:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">context </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;post is good!&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
472
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;saves the post and redirects to the HomePage&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
473
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">NewBlogPostForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">params:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
474
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> title:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;What a great post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
475
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> content:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;This post is the best post that has been written in the history of posts&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
476
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
477
- <span class="line"></span>
478
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> confidence_check { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) }</span></span>
479
- <span class="line"></span>
480
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">nil</span></span>
481
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> expect {</span></span>
482
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">handle!</span></span>
483
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> change { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">count</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">by</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
484
- <span class="line"></span>
485
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_redirected_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
486
- <span class="line"></span>
487
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">last</span></span>
488
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;What a great post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
489
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This post is the best post that has been written in the history of posts&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
490
- <span class="line"></span>
491
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
492
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This is using RSpec&#39;s <code>expect { ... }.to change { ... }.by(N)</code> to make sure that our handler created a row in the database. We then use the matcher <code>have_redirected_to</code> to assert that <code>result</code> is a URI to <code>HomePage</code>. We also check that the blog post we created in the database is correct.</p><p>Let&#39;s run the test with <code>bin/test run</code></p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> specs/front_end/handlers/new_blog_post_handler.spec.rb</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
493
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/handlers/new_blog_post_handler.spec.rb\\&quot;&quot;]</span></span>
494
- <span class="line"><span>Run options: exclude {e2e: true}</span></span>
495
- <span class="line"><span></span></span>
496
- <span class="line"><span>Randomized with seed 61034</span></span>
497
- <span class="line"><span></span></span>
498
- <span class="line"><span>NewBlogPostHandler</span></span>
499
- <span class="line"><span> post is not enough words</span></span>
500
- <span class="line"><span> re-generates the HTML for the BlogPostEditorPage, with server-side errors indicated</span></span>
501
- <span class="line"><span> post is good!</span></span>
502
- <span class="line"><span> saves the post and redirects to the HomePage</span></span>
503
- <span class="line"><span> #handle!</span></span>
504
- <span class="line"><span> client-side violations got to the server</span></span>
505
- <span class="line"><span> re-generates the HTML for the BlogPostEditorPage</span></span>
506
- <span class="line"><span></span></span>
507
- <span class="line"><span>Finished in 0.0138 seconds (files took 0.73976 seconds to load)</span></span>
508
- <span class="line"><span>3 examples, 0 failures</span></span>
509
- <span class="line"><span></span></span>
510
- <span class="line"><span>Randomized with seed 61034</span></span>
511
- <span class="line"><span></span></span>
512
- <span class="line"><span>[ bin/test ] [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/handlers/new_blog_post_handler.spec.rb\\&quot;&quot;] succeeded</span></span></code></pre></div><p>It passes!</p><p>Next, let&#39;s test <code>HomePage</code>.</p><h3 id="testing-pages" tabindex="-1">Testing Pages <a class="header-anchor" href="#testing-pages" aria-label="Permalink to &quot;Testing Pages&quot;">​</a></h3><p>Unlike our handler, which accepts arguments and returns a result, pages generate HTML. We are better off testing pages by asking them to generate HTML and then examining the HTML directly.</p><p>Brut provides the method <code>generate_and_parse</code> to generate a page&#39;s HTML, then use <a href="https://nokogiri.org/" target="_blank" rel="noreferrer">Nokogiri</a> to parse it. We can use CSS selectors on the result to assert things about the HTML.</p><p><code>mkbrut</code> created <code>specs/front_end/pages/home_page.spec.rb</code>, so let&#39;s open that up on your editor.</p><p>The way we&#39;ll write this test is to generate four random blog posts using our factory, request the page, then assert that each blog post is on the page.</p><p>Rather than assert that each blog post&#39;s text is just somewhere on the page, we&#39;ll make use of the <code>external_id</code> concept. We&#39;ll use it as the <code>id</code> attribute of the <code>&lt;article&gt;</code>.</p><p>Brut intends for you to use Nokogiri&#39;s API to access information about the parsed document, however it provides a few convenience methods. In the test below, you&#39;ll see <code>e!</code>, which is added to Nokogiri nodes.</p><p><code>e!</code> asserts that exactly one node matches the given CSS selector and returns that node. This makes it more expedient to access something that should be there, but fail with a useful error message when it&#39;s not.</p><p>Here&#39;s the test:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;spec_helper&quot;</span></span>
513
- <span class="line"></span>
514
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
515
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;should show the blog posts&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
516
- <span class="line"></span>
517
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> blog_posts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">4</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">times</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">map</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) }</span></span>
518
- <span class="line"></span>
519
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">generate_and_parse</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
520
- <span class="line"></span>
521
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">e!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;h1&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My Amazing Blog&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
522
- <span class="line"></span>
523
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> blog_posts.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">each</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |blog_post|</span></span>
524
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> post_article</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = result.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">e!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;article#</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">#{blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">external_id</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
525
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(post_article.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">e!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;h2&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
526
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> blog_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\n\\r</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">each</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |paragraph|</span></span>
527
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(post_article.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(paragraph)</span></span>
528
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
529
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
530
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
531
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Let&#39;s run the test, which should fail:</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> specs/front_end/pages/home_page.spec.rb</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
532
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/pages/home_page.spec.rb\\&quot;&quot;]</span></span>
533
- <span class="line"><span>Run options: exclude {e2e: true}</span></span>
534
- <span class="line"><span></span></span>
535
- <span class="line"><span>Randomized with seed 44491</span></span>
536
- <span class="line"><span></span></span>
537
- <span class="line"><span>HomePage</span></span>
538
- <span class="line"><span> should show the blog posts (FAILED - 1)</span></span>
539
- <span class="line"><span></span></span>
540
- <span class="line"><span>Failures:</span></span>
541
- <span class="line"><span></span></span>
542
- <span class="line"><span> 1) HomePage should show the blog posts</span></span>
543
- <span class="line"><span> Failure/Error: post_article = result.e!(&quot;article##{blog_post.external_id}&quot;)</span></span>
544
- <span class="line"><span></span></span>
545
- <span class="line"><span> article#blbl_6f04feaefb9520d86b19c3ac4ad22c4f matched 0 elements, not exactly 1:</span></span>
546
- <span class="line"><span></span></span>
547
- <span class="line"><span>«HUGE HTML DOCUMENT»</span></span>
548
- <span class="line"><span></span></span>
549
- <span class="line"><span> # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/enhanced_node.rb:32:in &#39;Brut::SpecSupport::EnhancedNode#e!&#39;</span></span>
550
- <span class="line"><span> # ./specs/front_end/pages/home_page.spec.rb:13:in &#39;block (3 levels) in &lt;top (required)&gt;&#39;</span></span>
551
- <span class="line"><span> # ./specs/front_end/pages/home_page.spec.rb:12:in &#39;Array#each&#39;</span></span>
552
- <span class="line"><span> # ./specs/front_end/pages/home_page.spec.rb:12:in &#39;block (2 levels) in &lt;top (required)&gt;&#39;</span></span>
553
- <span class="line"><span> # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:158:in &#39;block (2 levels) in Brut::SpecSupport::RSpecSetup#setup!&#39;</span></span>
554
- <span class="line"><span> # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:264:in &#39;Sequel::Database#_transaction&#39;</span></span>
555
- <span class="line"><span> # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:239:in &#39;block in Sequel::Database#transaction&#39;</span></span>
556
- <span class="line"><span> # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/connection_pool/timed_queue.rb:90:in &#39;Sequel::TimedQueueConnectionPool#hold&#39;</span></span>
557
- <span class="line"><span> # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/connecting.rb:283:in &#39;Sequel::Database#synchronize&#39;</span></span>
558
- <span class="line"><span> # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:197:in &#39;Sequel::Database#transaction&#39;</span></span>
559
- <span class="line"><span> # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:156:in &#39;block in Brut::SpecSupport::RSpecSetup#setup!&#39;</span></span>
560
- <span class="line"><span></span></span>
561
- <span class="line"><span>Finished in 0.54876 seconds (files took 0.73025 seconds to load)</span></span>
562
- <span class="line"><span>1 example, 1 failure</span></span>
563
- <span class="line"><span></span></span>
564
- <span class="line"><span>Failed examples:</span></span>
565
- <span class="line"><span></span></span>
566
- <span class="line"><span>bin/test run ./specs/front_end/pages/home_page.spec.rb:4 # HomePage should show the blog posts</span></span>
567
- <span class="line"><span></span></span>
568
- <span class="line"><span>Randomized with seed 44491</span></span>
569
- <span class="line"><span></span></span>
570
- <span class="line"><span>[ bin/test ] error: [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/pages/home_page.spec.rb\\&quot;&quot;] failed - exited 1</span></span></code></pre></div><p>Brut obviously errs on the side of being verbose. But, you can see that the problem is that it cannot find an <code>&lt;article&gt;</code> with the <code>id=</code> of <code>blbl_6f04feaefb9520d86b19c3ac4ad22c4f</code>, the <code>external_id</code> of the first blog post.</p><p>To make it pass, we&#39;ll need to add <code>id:</code> to each <code>&lt;article&gt;</code>. Make this one-line change in <code>HomePage</code>:</p><div class="language-diff vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">diff</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#B31D28;--shiki-dark:#FDAEB7;">- article do</span></span>
571
- <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">+ article(id: blog_post.external_id) do</span></span></code></pre></div><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>This shows a useful feature of the <code>external_id</code>: Because it&#39;s not only unique to the database table, but also across <em>all</em> database tables, it makes a pretty good <code>ID</code> inside an HTML page, since it&#39;s highly unlikely any other part of the page would use that value for the <code>id=</code> of an element.</p></div><p>Now, the test should pass:</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> run</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> specs/front_end/pages/home_page.spec.rb</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
572
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/pages/home_page.spec.rb\\&quot;&quot;]</span></span>
573
- <span class="line"><span>Run options: exclude {e2e: true}</span></span>
574
- <span class="line"><span></span></span>
575
- <span class="line"><span>Randomized with seed 56951</span></span>
576
- <span class="line"><span></span></span>
577
- <span class="line"><span>HomePage</span></span>
578
- <span class="line"><span> should show the blog posts</span></span>
579
- <span class="line"><span></span></span>
580
- <span class="line"><span>Finished in 0.53858 seconds (files took 0.69257 seconds to load)</span></span>
581
- <span class="line"><span>1 example, 0 failures</span></span>
582
- <span class="line"><span></span></span>
583
- <span class="line"><span>Randomized with seed 56951</span></span>
584
- <span class="line"><span></span></span>
585
- <span class="line"><span>[ bin/test ] [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; \\&quot;specs/front_end/pages/home_page.spec.rb\\&quot;&quot;] succeeded</span></span></code></pre></div><p>For <code>BlogPostEditorPage</code>, there really isn&#39;t anything to test - it&#39;s static HTML at this point. Even still, it&#39;s good to record a decision about testing code or not, so it&#39;s clear we didn&#39;t just forget. Brut provides the method <code>implementation_is_covered_by_other_tests</code> to do just that. It accepts a string where we can describe where the coverage for this class is.</p><p>In our case, it&#39;s going to be covered by an end-to-end test we&#39;ll write next.</p><p>Open up <code>specs/front_end/pages/blog_post_editor_page.spec.rb</code> and make it look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;spec_helper&quot;</span></span>
586
- <span class="line"></span>
587
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BlogPostEditorPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
588
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> implementation_is_covered_by_other_tests </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;end-to-end test&quot;</span></span>
589
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, all unit tests should pass, which we can check via <code>bin/test run</code>:</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> run</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
590
- <span class="line"><span>[ bin/test ] Running all tests</span></span>
591
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/&quot;]</span></span>
592
- <span class="line"><span>Run options: exclude {e2e: true}</span></span>
593
- <span class="line"><span></span></span>
594
- <span class="line"><span>Randomized with seed 63173</span></span>
595
- <span class="line"><span>...........</span></span>
596
- <span class="line"><span></span></span>
597
- <span class="line"><span>Finished in 0.53248 seconds (files took 0.7012 seconds to load)</span></span>
598
- <span class="line"><span>11 examples, 0 failures</span></span>
599
- <span class="line"><span></span></span>
600
- <span class="line"><span>Randomized with seed 63173</span></span>
601
- <span class="line"><span></span></span>
602
- <span class="line"><span>[ bin/test ] [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \\&quot;**/*.spec.rb\\&quot; /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/&quot;] succeeded</span></span></code></pre></div><p>As our last test, we&#39;ll write an end-to-end that uses a browser.</p><h3 id="writing-browser-end-to-end-tests" tabindex="-1">Writing Browser End-to-End Tests <a class="header-anchor" href="#writing-browser-end-to-end-tests" aria-label="Permalink to &quot;Writing Browser End-to-End Tests&quot;">​</a></h3><p>Browser tests are expensive and slow, but it&#39;s good to test entire workflows that catch issues that a unit test might not. In this case, we want to go through the steps of writing a blog post:</p><ol><li>Go to the post editor page and make sure client-side validations show us errors.</li><li>Submit a post that&#39;s too short and make sure server-side errors show up.</li><li>Submit a valid post and check that it makes it back to the home page.</li></ol><p>Brut uses <a href="https://playwright.dev/" target="_blank" rel="noreferrer">Playwright</a> to author end to end tests. Playwright is written in JavaScript, but there is a <a href="https://playwright-ruby-client.vercel.app/" target="_blank" rel="noreferrer">Ruby wrapper library</a> that alleviates us from having to worry about async/await style coding.</p><p>Ideally, we&#39;d use the same API here as we do in our page tests. Or, equally ideally, we&#39;d be able to use the API of the web platform. Playwright went a third way. Such is life.</p><p>The way this test will work is:</p><ol><li>Use <code>HomePage.routing</code> to kick everything off</li><li>Find a link to <code>BlogPostEditorPage.routing</code> on the page</li><li>Use Playwright&#39;s <code>page.locator</code> to find elements on the page to interact with (which will naturally wait for the page to load before doing so).</li><li>We&#39;ll use <code>fill</code> to fill in data for the form fields and <code>click</code> to submit the form by clicking the submit button.</li><li>The matcher <code>have_text</code> will be used assert that text appears inside an element.</li></ol><p>Brut provides the matcher <code>be_page_for</code> to assert that we are viewing the page we think we are. Nothing is more frustrating than watching assertions fail because your test ended up on the wrong page.</p><p>Open up <code>specs/e2e/home_page.spec.rb</code> and replace it with this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;spec_helper&quot;</span></span>
603
- <span class="line"></span>
604
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Posting blog posts&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
605
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;allows posting a post&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
606
- <span class="line"></span>
607
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # 1. Go to the blog post editor page from the home page</span></span>
608
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">goto</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
609
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> new_post_link</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;a[href=&#39;</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">#{</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&#39;]&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
610
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> new_post_link.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span></span>
611
- <span class="line"></span>
612
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(page).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> be_page_for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
613
- <span class="line"></span>
614
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # 2. Provide data that violates client-side constraints and check for error messages</span></span>
615
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> title_field</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut-form input[name=&#39;title&#39;]&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
616
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> content_field</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut-form textarea[name=&#39;content&#39;]&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
617
- <span class="line"></span>
618
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title_field.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;XX&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
619
- <span class="line"></span>
620
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> submit_button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut-form button&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
621
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> submit_button.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span></span>
622
- <span class="line"></span>
623
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(page).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> be_page_for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
624
- <span class="line"></span>
625
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> title_error_message</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut-cv-messages[input-name=&#39;title&#39;] brut-cv&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
626
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> content_error_message</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut-cv-messages[input-name=&#39;content&#39;] brut-cv&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
627
- <span class="line"></span>
628
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(title_error_message).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This field is too short&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
629
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(content_error_message).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This field is required&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
630
- <span class="line"></span>
631
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # 3. Provide data that passes client-side constraints but violates server-side ones</span></span>
632
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title_field.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;New blog post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
633
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> content_field.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Too short&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
634
- <span class="line"></span>
635
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> submit_button.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span></span>
636
- <span class="line"></span>
637
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(page).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> be_page_for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPostEditorPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
638
- <span class="line"></span>
639
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(content_error_message).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This field does not have enough words&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
640
- <span class="line"></span>
641
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # 4. Provide a valid blog post</span></span>
642
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> content_field.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This is a longer post, so we should be OK&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
643
- <span class="line"></span>
644
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> submit_button.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span></span>
645
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(page).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> be_page_for</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
646
- <span class="line"></span>
647
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> new_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">order</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">desc</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:created_at</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">first</span></span>
648
- <span class="line"></span>
649
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> article</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;article#</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">#{new_post.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">external_id</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">}</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
650
- <span class="line"></span>
651
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(article).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;New blog post&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
652
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(article).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;This is a longer post, so we should be OK&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
653
- <span class="line"></span>
654
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
655
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Run it now with <code>bin/test e2e</code>:</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you stopped your entire dev environment (<code>dx/start</code>), when you restart it, you <em>must</em> re-run <code>bin/setup</code>, since the disk inside your dev environment is ephemeral.</p></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/test</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> e2e</span></span></code></pre></div><p>It should pass:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
656
- <span class="line"><span>[ bin/test ] Rebuilding test database schema</span></span>
657
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/db rebuild --env=test&quot;]</span></span>
658
- <span class="line"><span>[ bin/db ] Database exists. Dropping...</span></span>
659
- <span class="line"><span>[ bin/db ] blog_test does not exit. Creating...</span></span>
660
- <span class="line"><span>[ bin/db ] Migrations applied</span></span>
661
- <span class="line"><span>[ bin/test ] [&quot;bin/db rebuild --env=test&quot;] succeeded</span></span>
662
- <span class="line"><span>[ bin/test ] Running all tests</span></span>
663
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag e2e -P \\&quot;**/*.spec.rb\\&quot; /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/&quot;]</span></span>
664
- <span class="line"><span>Run options: include {e2e: true}</span></span>
665
- <span class="line"><span></span></span>
666
- <span class="line"><span>Randomized with seed 27681</span></span>
667
- <span class="line"><span>[ bin/test-server ] Building assets</span></span>
668
- <span class="line"><span>«TONS OF OUTPUT»</span></span>
669
- <span class="line"><span>[ bin/test-server ] Starting server</span></span>
670
- <span class="line"><span>[ bin/run ] No pidfile-Starting up</span></span>
671
- <span class="line"><span>[3352] Configuration:</span></span>
672
- <span class="line"><span>«TONS OF OUTPUT»</span></span>
673
- <span class="line"><span>[3352] Use Ctrl-C to stop</span></span>
674
- <span class="line"><span>[3352] - Worker 0 (PID: 3361) booted in 0.0s, phase: 0</span></span>
675
- <span class="line"><span>[3352] - Worker 1 (PID: 3364) booted in 0.0s, phase: 0</span></span>
676
- <span class="line"><span>.[3352] === puma shutdown: 2025-08-11 22:18:16 +0000 ===</span></span>
677
- <span class="line"><span>[3352] - Goodbye!</span></span>
678
- <span class="line"><span>[3352] - Gracefully shutting down workers...</span></span>
679
- <span class="line"><span></span></span>
680
- <span class="line"><span></span></span>
681
- <span class="line"><span>Finished in 3.45 seconds (files took 0.69401 seconds to load)</span></span>
682
- <span class="line"><span>1 example, 0 failures</span></span>
683
- <span class="line"><span></span></span>
684
- <span class="line"><span>Randomized with seed 27681</span></span>
685
- <span class="line"><span></span></span>
686
- <span class="line"><span>[ bin/test ] [&quot;bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag e2e -P \\&quot;**/*.spec.rb\\&quot; /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/&quot;] succeeded</span></span>
687
- <span class="line"><span>[ bin/test ] Re-Rebuilding test database schema</span></span>
688
- <span class="line"><span>[ bin/test ] Executing [&quot;bin/db rebuild --env=test&quot;]</span></span>
689
- <span class="line"><span>[ bin/db ] Database exists. Dropping...</span></span>
690
- <span class="line"><span>[ bin/db ] blog_test does not exit. Creating...</span></span>
691
- <span class="line"><span>[ bin/db ] Migrations applied</span></span>
692
- <span class="line"><span>[ bin/test ] [&quot;bin/db rebuild --env=test&quot;] succeeded</span></span></code></pre></div><p>With that test done, <code>bin/ci</code>, which we ran at the start, should run all tests, plus check for CVEs in our installed gems.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/ci</span></span></code></pre></div><p>It should also pass:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
693
- <span class="line"><span>«TONS OF OUTPUT»</span></span>
694
- <span class="line"><span>[ bin/ci ] Analyzing Ruby gems for</span></span>
695
- <span class="line"><span>[ bin/ci ] security vulnerabilities</span></span>
696
- <span class="line"><span>Updating ruby-advisory-db ...</span></span>
697
- <span class="line"><span>From https://github.com/rubysec/ruby-advisory-db</span></span>
698
- <span class="line"><span> * branch master -&gt; FETCH_HEAD</span></span>
699
- <span class="line"><span>Already up to date.</span></span>
700
- <span class="line"><span>Updated ruby-advisory-db</span></span>
701
- <span class="line"><span>ruby-advisory-db:</span></span>
702
- <span class="line"><span> advisories: 998 advisories</span></span>
703
- <span class="line"><span> last updated: 2025-08-08 10:26:18 -0700</span></span>
704
- <span class="line"><span> commit: 43149b540b701c9683e402fcd7fa4e5b6e5b60e9</span></span>
705
- <span class="line"><span>No vulnerabilities found</span></span>
706
- <span class="line"><span>[ bin/ci ] Checking to see that all classes have tests</span></span>
707
- <span class="line"><span>[ bin/test ] All tests exists!</span></span>
708
- <span class="line"><span>[ bin/ci ] Done</span></span></code></pre></div><p>That&#39;s it!</p><h2 id="areas-for-self-exploration" tabindex="-1">Areas for Self-Exploration <a class="header-anchor" href="#areas-for-self-exploration" aria-label="Permalink to &quot;Areas for Self-Exploration&quot;">​</a></h2><p>Here are a few enhancement you can try to make:</p><ul><li>Create a client-side constraint requiring the title to match a certain regexp.</li><li>Add a server-side constraint requiring at least two paragraphs.</li><li>Allow editing the blog post creation date</li><li>Add an author field to allow entering the author&#39;s name</li><li>Add pagination to the home page</li></ul>`,330)])])}const f=i(E,[["render",u]]);export{B as __pageData,f as default};