Test the HTML your app returns.
Describe rendered elements with Blueprint-shaped expectations while Corretto drives real requests and isolated database state.
Start with a real response
Generated spec helpers require caramel/corretto and call Corretto.configure(App). Inside an example, use a session to request the page and assert its observable output.
shown = client.get(path)
shown.should have_status(200)
shown.should render_page("Book")
shown.should have_html {
section(class: "form-page") {
h1 { "Book" }
dl { dd { "<Dune>" } }
a(href: "#{path}/edit") { "Edit book" }
}
}The block records requirements independently of the view renderer. Expected strings are decoded literal text: "<Dune>" checks escaped user text. Build expectations from values and elements, rather than rendering an application view as the expected result.
Select the requirements that matter
Each block has one root. By default, extra wrappers, markup and attributes are allowed. Nested requirements belong to the same matched ancestor; sibling requirements consume distinct nodes. Text matches exactly after HTML whitespace normalization, while pre and textarea preserve whitespace. Classes match a token subset, and Boolean attributes check presence or absence. Use strings for literal ARIA/data values such as aria_expanded: "false".
shown.should have_html(within: "#books", count: 2) { li(class: "book") }
shown.should have_html(count: 0) { script(src: "/unexpected.js") }
shown.should_not have_html { p { "Access denied" } }Numeric, Boolean, and character leaf values check their literal text. Convert other values with .to_s. Use plain for direct text beside child elements. Loops and captured local values work; use element("book-card") for custom tags. A misspelled standard element fails compilation.
Check structure and partial responses
strict: true compares complete attributes and ordered direct children, ignoring comments and insignificant formatting whitespace. Use it when the complete subtree is the contract.
shown.should have_html(strict: true) {
ul(class: "books") {
li { "Dune" }
li { "Solaris" }
}
}The block overload of render_partial checks target, swap, and content in the same hx-partial envelope. HTML5 table and select contexts are supported, including rows or cells inside partial envelopes.
created.should render_partial("#note-list", swap: "beforeend") {
li { "<b>Milk</b> ×2" }
}render_page("Books") requires an actual source doctype and a parsed title containing Books. Both plain response matchers and block expectations are available. Invalid selectors, missing scopes, and invalid patterns raise even for negative assertions. Keep exact-byte tests for serialization and browser tests for JavaScript and htmx morphing.
Install the test dependencies
Generated applications declare Lexbor 3.6.4 as a development dependency. Keep this pin and the matching lockfile entry when configuring a project; frappe setup uses a frozen install. Development installs build the native parser with cc and ar, and need network access for its verified source download. Production installs omit the parser.
development_dependencies:
lexbor:
github: kostya/lexbor
version: 3.6.4Use frappe lint for the scoped paragraph and nested-block rules in expectations. Upstream Ameba editor integrations alone do not load Caramel’s custom rules.
frappe check
frappe corretto spec/requests/books_spec.cr
frappe lint