Skip to content

Testing applications ​

Require crykit/testing from your application's Crystal specs to run the real application with in-memory window, dialog, menu, tray, and system backends. The testing runtime loads the application's routes and templates, mounts its views, and dispatches actions through CryKit. It does not open desktop windows.

The testing API ships with the CryKit shard. Application specs need no files from CryKit's own spec/ directory.

A complete example ​

Keep application definitions separate from the entry point that calls run, so specs can require the application without launching it. Save this as src/app.cr:

crystal
require "crykit"

class Counter
  include CryKit::View

  reactive count : Int32 = 0

  template "counter"
end

class CounterApp < CryKit::Application
  protected def configure : Nil
    routes.draw do
      get "/" do
        mount Counter.new
      end
    end
  end

  protected def register_windows : Nil
    windows.create(id: :main, title: "Counter", path: "/")
  end
end

Save the view's template as src/counter.ecr:

ecr
<main>
  <p id="count"><%= reactive.count %></p>
  <button id="increment" on_click=<%~ -> { reactive.count += 1 } %>>Increment</button>
</main>

Save the spec as spec/counter_spec.cr:

crystal
require "spec"
require "crykit/testing"
require "../src/app"

it "increments the displayed count" do
  CryKit::Testing.run(CounterApp) do |test|
    page = test.window(:main)
    page.element("count").text.should eq("0")
    page.click("increment")
    page.element("count").text.should eq("1")
  end
end

From your application directory, run:

sh
shards install
crystal spec

For normal desktop startup, put require "./app" and CounterApp.new.run in src/main.cr. The spec uses the same application class, routes, and templates with injected test backends. If your application constructor accepts additional objects, create the runtime explicitly and pass its backends alongside those arguments:

crystal
runtime = CryKit::Testing::Runtime.new
app = CounterApp.new(**runtime.backends)
runtime.run(app) do |test|
  test.window(:main).element("count").text.should eq("0")
end

Inspecting and interacting with a window ​

test.window(:main) selects the window by its application ID. Page methods use HTML id attributes, so give interactive elements and assertion targets stable IDs in their templates.

APIPurpose
page.click("increment")Dispatch a click to the element's CryKit callback and apply its updates.
page.input("name", "Ada")Dispatch an input value to a bound input or its input callback.
page.keydown("editor", "Enter", meta_key: true)Dispatch a captured keyboard event to the element's keydown callback.
page.keyup("editor", "Enter")Dispatch a captured keyboard event to the element's keyup callback.
page.submit("signup", {name: "Ada"})Submit form data to its typed or raw form callback.
page.htmlRead the current mounted view contents as HTML.
page.element("count")Find a required element; raises if the ID is missing.
page.find("optional-message")Find an optional element; returns nil when absent.
page.element("count").textRead an element's text, including descendant text.
page.element("next").attribute("disabled")Read an HTML attribute; returns nil when absent.

Keyboard helpers accept code, location, alt_key, ctrl_key, meta_key, shift_key, repeat, and is_composing as named arguments. Defaults are an empty code, location 0, and false flags. Locations must be integers from 0–3. Like other headless actions, these helpers target the callback's bound HTML ID directly; they do not simulate focus, event bubbling, or browser default actions. Use page.input to update bound text before sending a keyboard shortcut.

Elements are snapshots. Inspect the page again after an action to assert the resulting UI. For example:

crystal
page.click("dismiss-message")
page.find("message").should be_nil

Runtime#run starts and shuts down the application within an isolated test context. Cleanup also runs when an assertion raises. Create a fresh runtime and application for each test; no manual view mounting, event queue management, or application shutdown is needed.

Dialogs and file drops ​

Set the dialog backend's selection before invoking the application action that opens a picker. The normal application code still calls app.dialogs.open_file:

crystal
CryKit::Testing.run(MyApp) do |test|
  page = test.window(:main)
  test.dialogs.selection = "/path/to/sample.sqlite3"
  page.click("open-database")
  page.find("table-list").should_not be_nil

  test.dialogs.selection = nil # Cancel the next single-file picker.
  page.click("change-database")
end

For a file-drop handler, enable file_drop: true on the application window and declare a target such as file_drop_target="imports" in its template. Supply local paths and the target name in the spec:

crystal
page.drop_files([sample_path], target: "imports").should be_true

The helper returns whether the drop was accepted. It requires a declared target in the current rendered page; it does not simulate drag coordinates or hover styling. Dialog calls are recorded in test.dialogs.calls; use selections for multi-file pickers, confirmation for confirm dialogs, and question_response for a custom question's button ID.

Use real temporary fixture files when the application reads their contents. Create and remove those files in the spec; the testing runtime does not replace filesystem or database access. The DB Studio specs demonstrate selecting SQLite fixtures, opening table tabs, pagination, and failed-file handling.

Background work ​

The test runtime queues submitted worker jobs instead of starting native workers. Run queued work explicitly, then inspect the published UI updates:

crystal
page.click("start-import")
test.run_jobs
page.element("import-status").text.should eq("Complete")

This lets a spec assert the state before and after a job without timing sleeps. Jobs still use the application's normal worker and notification APIs. Time is not virtualized: sleeps take real time, and recurring or never-ending work should be tested separately. test.run_jobs(limit: 100) can bound the number of queued tasks a test permits.

What these tests cover ​

These tests exercise application setup, routing, compiled templates, reactive updates, view lifetimes, and capability callbacks through test backends. They are useful for checking application behavior in ordinary crystal spec runs.

page.html contains mounted view markup; document layouts and assets are not rendered or executed by a browser. Browser layout, CSS, JavaScript execution, native dialogs, and operating-system file dragging require browser or desktop checks. Passing a backend test does not establish those behaviors. See Contributing for CryKit's browser and desktop verification tools.

Built with Crystal. Documented with VitePress.