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:
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
endSave the view's template as src/counter.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:
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
endFrom your application directory, run:
shards install
crystal specFor 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:
runtime = CryKit::Testing::Runtime.new
app = CounterApp.new(**runtime.backends)
runtime.run(app) do |test|
test.window(:main).element("count").text.should eq("0")
endInspecting 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.
| API | Purpose |
|---|---|
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.html | Read 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").text | Read 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:
page.click("dismiss-message")
page.find("message").should be_nilRuntime#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:
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")
endFor 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:
page.drop_files([sample_path], target: "imports").should be_trueThe 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:
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.