Skip to main content

Writing and Running Tests

A Hosanna regression test is regular Vitest TypeScript with an hs fixture. The fixture talks to the running application's test command plane; it does not query implementation-specific browser DOM paths.

Test anatomy

integration/smoke/launch.test.ts
import { test, expect } from '../fixture';

test('starts an isolated sample app and exposes the command plane', async ({ hs }) => {
const status = await hs.wait.forData(
() => hs.app.status(),
value => value?.phase === 'ready',
{ timeoutMs: 30_000, stableMs: 200, description: 'ready test status' },
);
expect(status).toMatchObject({
phase: 'ready',
testRunId: hs.descriptor.testRunId,
appId: hs.descriptor.appId,
});
expect((await hs.views.hierarchy()).length).toBeGreaterThan(0);
await expect(hs.logs).toHaveNoErrors();
});

The launch handshake protects against accidentally attaching to an old process: app ID, test-run ID, and ready phase must match before tests begin.

Prefer semantic locators

Use stable application IDs or explicit test IDs:

const menu = hs.control.byId('sideMenu');
const card = hs.control.byTestId('featured-card');

state() requires one match. If a repeated component intentionally shares an ID, scope it in application data or use allById and assert why a particular item is selected. Do not use a DOM selector or serialized hierarchy position.

Images have equivalent locators and CollectionView-aware lookup:

const image = hs.image.inCollectionCell({
collectionId: 'collectionView',
rowId: 'featured',
cellIndex: 0,
imageId: 'poster',
});

await expect(image).toBeLoadedImage({ minWidth: 1, minHeight: 1 });

Wait for outcomes

The client includes waits for screens, controls, focus, properties, logs, arbitrary data, images, and an idle hierarchy/focus frame.

await hs.remote.press('Right');
await hs.wait.forFocus({ id: 'secondCard' }, {
stableMs: 200,
description: 'second card after remote navigation',
});

Use hs.pause(milliseconds, reason) only when the external system provides no observable readiness signal. The required reason makes exceptional timing waits visible in command diagnostics.

Reset state with preconditions

The fixture applies defaultPrecondition before each test. A precondition can clear or seed the registry, relaunch the app, and wait for its known initial screen.

const cleanLaunch = async ({ hs }) => {
await hs.registry.clear();
await hs.app.relaunch();
await hs.wait.forScreen('springboard', {
stableMs: 250,
description: 'clean sample springboard',
});
};

Register it in hosanna-test.config.ts, then apply another state explicitly when a test needs it:

await hs.state.apply('restoredSelection');

Applications can use this pattern for signed-out, privacy-required, incompatible-version, or subscriber states. Application state names and registry keys do not belong in Hosanna Tools.

Inputs and assertions

The public client includes:

  • hs.remote.press, pressSequence, keyDown, and keyUp;
  • hs.touch.tap, tapAt, and swipe when the owned browser context has touch enabled;
  • hs.text.enter on browser targets;
  • hs.control, hs.focus, hs.screen, hs.collection, hs.image, and hs.logs assertions;
  • hs.app.reload, relaunch, and deepLink where the target supports them.

Check hs.app.capabilities() or select the target before calling target-specific APIs. A capability error is preferable to silently skipping an interaction.

The current sample suite uses its browser-only CDP helper for touch because a browser reached through connectOverCDP can report touch capability while Playwright's touchscreen.tap still rejects the reattached context when hasTouch was not enabled at context creation. The helper dispatches real CDP touch events at semantic control bounds; it is an explicit host workaround, not a replacement locator API.

Run the suite or one scenario

The project package scripts are the source of truth. The standard Vitest forwarding forms are:

# Standard web suite
npm run test:ui:web

# One test file
npm run test:ui:web -- smoke/launch.test.ts

# One test name
npm run test:ui:web -- -t "starts an isolated sample app"

# Watch while authoring
npm run test:ui:watch

# Type-check the integration project without launching the app
npm run test:ui:typecheck

Use the sample scripts for headed and verbose debug runs:

npm run test:ui:headed -- smoke/launch.test.ts
npm run test:ui:debug -- smoke/launch.test.ts

For deeper diagnostics, add HOSANNA_TEST_VERBOSE=1. Vitest's normal debugger options still apply, but the launched application is a separate owned runtime.

Update expected images deliberately

Visual comparison is opt-in in the reference applications. Review a failure first; then create or replace the baseline only when the UI change is intentional:

npm run test:ui:update

test:ui:update limits the run to the sample visual tests and sets HS_UPDATE_SCREENSHOTS=1.

Inspect both the new baseline and the rendered state captured in the report before committing it. See Images and Visual Regression.

Keep each test independent

  • Begin from a named precondition.
  • Assert the postcondition of each action before sending the next one.
  • Do not rely on test ordering.
  • Avoid mutable application data shared with another concurrent run.
  • Keep fileParallelism: false while the suite owns a single runtime.
  • Put repeated application navigation in integration/support/; keep generic lifecycle and artifact behavior in Hosanna Tools.

Next: Reports and Failure Investigation.

Reference tests: startup, navigation/history, input methods, and collection selection.

Talk to us