# Testing A view is a Ruby object, so a view test builds one and calls it. The only thing it needs that an ordinary object does not is a view context, because `path`, `assets`, `flash` and the rest are read off the context Hanami builds for each request. phlex-hanami ships helpers that build one for you. The helpers are three methods in `Phlex::Hanami::Testing::ViewHelpers`, and none of them know what test framework you use. RSpec gets a file that wires them up for you. Anything else includes the module. ## Setup with RSpec Require it from `spec/spec_helper.rb`, or from a file under `spec/support`, which the spec helper [hanami-rspec] generates already loads: ```ruby # spec/support/phlex.rb require "phlex/hanami/rspec" ``` That is the whole integration. hanami-rspec is a generator gem and adds nothing at runtime, so there is nothing to order this against. The helpers go into every example group tagged `type: :view`. Tag component specs the same way, since a component is a view as far as this is concerned. To pick them up somewhere else, include them by hand: ```ruby RSpec.configure do |config| config.include Phlex::Hanami::Testing::ViewHelpers end ``` ## Setup with Minitest `hanami new --test=minitest` generates a `test/support/minitest.rb` that reopens the base test class for helpers. Include the module there, and every test gets them: ```ruby # test/support/minitest.rb require "phlex/hanami/testing" class Hanami::Minitest::Test include Phlex::Hanami::Testing::ViewHelpers end ``` `render` is a common enough name that you may not want it on every test in the suite. Include it into a base class for view tests instead: ```ruby # test/support/view_test.rb require "phlex/hanami/testing" class ViewTest < Hanami::Minitest::Test include Phlex::Hanami::Testing::ViewHelpers end ``` The examples below are written for RSpec. Every helper works the same under Minitest; only the assertions change. ## A view spec ```ruby # spec/my_app/views/posts/index_spec.rb RSpec.describe MyApp::Views::Posts::Index, type: :view do subject(:html) { render(described_class.new(posts: posts)) } let(:posts) { [Post.new(title: "First")] } it "lists the posts" do expect(html).to include("
  • First
  • ") end it "links to each post" do expect(html).to include(%()) end end ``` No request runs. `render` builds a context from the view's own slice, so `path` and `assets` give the same answers they give in production, and a view in the `admin` slice gets the `admin` slice's routes. `render` renders the instance you hand it, so no layout is applied and you are asserting on the view's own markup. To render a view inside its layout the way Hanami does, call the class: ```ruby described_class.call(context: view_context, posts: posts) ``` There are no matchers. The output is a String, and `include`, `match` and whatever HTML matcher you already like all work on it. ## A component spec The same, with a block if the component takes one: ```ruby RSpec.describe MyApp::Components::Card, type: :view do it "wraps its content" do html = render(described_class.new(title: "Hello")) { "body" } expect(html).to include("

    Hello

    ") expect(html).to include("body") end end ``` A plain `Phlex::HTML` class that never heard of Hanami renders through `render` too. It has no slice, so it gets a bare context, which answers `content_for` and raises a clear error for anything else. ## Views that need a request `session`, `flash` and `csrf_token` come from the request. `view_request` builds one without running an action: ```ruby RSpec.describe MyApp::Views::Layout, type: :view do it "shows the flash" do request = view_request("/posts", flash: { alert: "Not saved" }) expect(render(described_class.new, request: request)).to include("Not saved") end end ``` `view_request` takes a path, and `session:`, `flash:` and `csrf_token:` for the three things a view reads off it. Anything else goes to `Rack::MockRequest.env_for`, so headers and a request method are available when a view cares about them. Sessions are always on, so a view reading an empty session gets nil rather than an error. ## Building the context yourself `view_context` returns the context `render` would have built, for the times you want to set something on it first: ```ruby it "reads content set elsewhere" do context = view_context context.content_for(:page_title, "Posts") expect(render(described_class.new, context: context)).to include("Posts") end ``` It takes `slice:` to build from a slice other than the app, and `request:` to attach a request. The class it builds is the one an action would build: a `Hanami::View::Context` subclass when hanami-view is bundled, otherwise `Phlex::Hanami::Context`. A view reads the same either way. Hanami turns off container memoization in the test env so that components can be stubbed. Each context resolves its routes, assets and i18n backend from the container as it is built, rather than holding a set from an earlier example. [hanami-rspec]: https://github.com/hanami/rspec