Skip to main content
zerotal

Testing

@zerotal/admin/testing mounts a resource's pages on Flow's in-process test harness and adds assertions phrased in admin terms — columns, fields, actions and records — so a test reads like a description of the screen rather than a walk through its markup.

Nothing is served over the network. Mounting a page instantiates the component, runs its real lifecycle, and renders once, so there is no server to boot and no WebSocket to connect. Tests stay fast enough to cover every resource you register.

Mounting a page

Three helpers cover the three screens a resource renders. Each returns a FlowTest, so every assertion in Flow's testing guide works alongside the admin-specific ones below.

HelperScreenReach for it to check
AdminTest.list(Resource, props?)List pageColumns, search, sorting, filters, bulk actions
AdminTest.view(Resource, id)View pageInfolist entries and header actions for a record
AdminTest.form(Resource, mode?)Create / Edit formFields, validation and saving
// tests/admin/users.test.ts
import { AdminTest, assertHasColumn, assertHasAction } from "@zerotal/admin/testing";
import { UserResource } from "../../app/admin/UserResource.ts";

const list = await AdminTest.list(UserResource);
assertHasColumn(list, UserResource, "email");
assertHasAction(list, "Create");
list.assertSee("ada@example.com");

AdminTest.form() takes the mode as its second argument — "create" (the default) or "edit". Drive the form through its form property, which holds every field's value:

const form = await AdminTest.form(UserResource, "create");
assertHasField(form, UserResource, "name");
await form.set("form", { name: "" });
await form.call("save");
form.assertHasErrors("name");

Seeding list state

The List page keeps search, sorting, pagination and filters in @url state. Pass those values as the second argument to AdminTest.list() and the page mounts as though the reader had arrived on that URL — which is how you assert on a filtered or sorted table without first driving the clicks that would produce it.

const list = await AdminTest.list(UserResource, {
  search: "ada",
  sortBy: "createdAt",
  sortDir: "desc",
  page: "2",
});
PropSeeds
searchThe search box
sortBy / sortDirSort column and direction
page / perPagePagination
filtersActive filter values
tabThe selected tab
trashedThe soft-delete scope
groupActive grouping
colsColumn visibility toggles
localeThe locale translated values use

Every one of these is a string, because they round-trip through the query string.

Admin assertions

These four take a key from the resource definition and assert against the resolved label. A test written this way keeps passing when a column is relabelled through .label(), and fails when the column is removed — usually the change you wanted the test to catch.

AssertionPasses when
assertHasColumn(t, Resource, key)That column's header is rendered
assertHasField(t, Resource, key)That form field's label is rendered
assertHasAction(t, label)An action with that label is on the page
assertSeesRecord(t, Resource, record)The record's title appears in a row

assertSeesRecord composes the title through the resource's recordTitleAttribute, so it finds the row however that title is assembled. assertHasField searches through nested layout components, so a field inside a tab or section is found without naming its container.

Driving and inspecting the page

The harness inherits Flow's actions and assertions. Two are worth calling out, because reaching for the wrong one hides bugs:

  • set(prop, value) assigns a property directly and skips the updating/updated hooks. Use it to arrange state before the behaviour you are actually testing.
  • update(prop, value) takes the same path a change from the browser does and fires those hooks. Use it when the hooks are the behaviour under test — a filter that refetches rows, or a field that derives another field.

call(method, ...args) invokes an action the way a button would. For assertions, assertSee / assertDontSee cover rendered text, assertHasErrors(field) and assertNoErrors() cover validation, assertRedirectedTo(url) covers a save that navigates away, and assertFlashed(level?, message?) covers the notification a successful action leaves behind.

When an assertion fails and you need to see why, html() returns the rendered markup, errors() the validation bag, and page() the live component instance.

Testing a non-default panel

Every helper accepts a panel as its last argument, defaulting to Panel.default(). Apps that register more than one panel pass the one under test, so the page resolves that panel's own configuration and navigation:

const shop = Panel.get("shop");

const list = await AdminTest.list(OrderResource, {}, shop);
const form = await AdminTest.form(OrderResource, "edit", { recordId: "1" }, shop);

Next steps

  • Admin overview — the guide's front page and the rest of the sections.
  • Flow testing — the harness these helpers are built on.