Skip to content
carameldocs · 0.16.0
Browse documentation
Guides / Testing HTML

Test the HTML your app returns.

Describe rendered elements with Blueprint-shaped expectations while Corretto drives real requests and isolated database state.

ContractValue
NeedsA generated application that has run frappe migrate; it declares the lexbor shard 3.6.4 as a development dependency
You will haveRequest specs that assert rendered elements against isolated database state
Covershave_html, render_page, render_partial, strict: true and scoped matching
Proved byfrappe check · frappe corretto · frappe lint

Start with a real response

The generated spec/spec_helper.cr already configures Corretto; the reference lists what it does. This guide works on a Book resource and the request spec its generator writes.

Read the Corretto reference →Submit a rendered form →
Terminal · inside your application
frappe make resource Book title:string
frappe migrate

Expect: frappe make resource prints Generated for the model, changeset, actions, views, spec/requests/books_spec.cr, a migration, config/routes.cr and config/paths.cr, then Run frappe migrate to apply the new schema. frappe migrate ends with The database matches the declared schema.

Inside an example, a session requests the page and the expectation describes what the response must contain. Add this example to the generated spec:

spec/requests/books_spec.cr · a second example
it "shows a book with its title escaped" do
  Corretto.session do |client, db|
    created = client.post("/books", params: {"title" => "<Dune>"})
    path = "/books/#{App::Book.query.order_by(:id, :desc).first!(db).id}"
    created.should redirect_to(path)
    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" }
      }
    }
  end
end

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

RuleWhat matches
Rootseach block has one root
Wrappers and attributesextra wrappers, markup and attributes are allowed by default; a nested requirement may sit below extra wrappers
Ordersibling order does not matter; nested requirements belong to the same matched ancestor; sibling requirements consume distinct nodes
Textexact after HTML whitespace normalization: space, tab, line feed, form feed and carriage return collapse to one space, with surrounding spaces removed; nonbreaking spaces stay distinct, and pre and textarea preserve whitespace
Classesmatch a token subset
Boolean attributescheck presence or absence
Arraysflatten, omit nil and join with spaces
Nested keywordsbecome hyphenated names: data: {book_id: 7} checks data-book-id="7"
ARIA and data valuesuse strings, such as aria_expanded: "false"
Corretto · scopes and counts
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. Numeric loop return values are ignored when the block has recorded child requirements. Convert other values with .to_s: an unsupported value raises instead of silently skipping the text check, while empty collection loops and nil add no text requirement. Use plain for direct text beside child elements; comments do not split adjacent text, but an actual intervening element does. Loops and captured local values work; use element("book-card") for custom tags. Blueprint’s standard element names apply, including select_tag for select.

Check structure and partial responses

strict: true compares complete attributes and ordered direct children, ignoring comments and insignificant formatting whitespace. Classes stay an unordered set. Use it when the complete subtree is the contract; for mixed content, put plain between element calls.

Corretto · a complete subtree
shown.should have_html(strict: true) {
  ul(class: "books") {
    li { "Dune" }
    li { "Solaris" }
  }
}

The block overload of render_partial checks the fragment’s content inside the same hx-partial envelope as its target and swap. HTML5 table and select contexts are supported, including rows or cells inside partial envelopes.

Corretto · fragment response
created.should render_partial("#note-list", swap: "beforeend") {
  li { "<b>Milk</b> ×2" }
}

Attribute order, quoting and entity spelling do not affect these assertions. Both plain response matchers and block expectations are available; the reference lists each matcher and what it requires.

These raise even for negative assertions:

ConditionOutcome
A negative count:raises ArgumentError
within: matches no elementraises ArgumentError
A block with no root, or more than oneraises ArgumentError
HTML Lexbor cannot parseraises ArgumentError
An invalid selectorraises

Install the test dependencies

Generated applications declare the lexbor shard 3.6.4 as a development dependency; it builds liblexbor 3.0.0 from a SHA-256-verified source download with cc and ar, which needs network access and the C toolchain your local environment installs. Keep this pin and the matching lockfile entry; frappe setup uses a frozen install. Production installs (shards install --production) neither restore nor build the parser, and only Corretto links it.

shard.yml · development dependencies
development_dependencies:
  lexbor:
    github: kostya/lexbor
    version: 3.6.4

An application made before 0.6.0 adds the same dependency, then refreshes shard.lock with the framework’s Shards launcher, because frappe setup does not create missing lock entries:

Terminal · inside your application
lib/caramel/scripts/shards install
frappe setup

Expect: shard.lock gains a lexbor entry at 3.6.4 and lib/lexbor appears. frappe setup ends with NAME is configured. Run frappe dev.

What does not compile

Each mistake below stops the build.

ConditionOutcome
A misspelled standard element, such as h11fails compilation
A Lexbor shard version other than 3.6.4fails compilation with Review Corretto’s native template layouts before changing the pinned Lexbor shard

Not covered yet

  • HTML5 parsing repairs malformed HTML as a browser does; it is not a conformance validator.
  • Exact-byte tests for serialization and browser tests for JavaScript and htmx morphing stay separate.
  • Upstream Ameba editor integrations alone do not load Caramel’s custom rules.

Prove it

Run the expectations

frappe lint exempts the paragraph p and multi-line curly blocks inside have_html and render_partial. Debug calls elsewhere, and pp, p! or dump inside an expectation, are still reported.

Terminal
frappe check
frappe corretto spec/requests/books_spec.cr
frappe lint

Passing: frappe check exits 0. frappe corretto ends with Corretto: 1 of 1 workers passed. frappe lint exits 0: no debug call outside an expectation, and no pp or p! inside one.