# Test an application

A successful build proves that the source was accepted. It does not prove that a
button works, a request was validated or an island adopted the server DOM. Test
those boundaries directly. The [learning examples](/learn/) use their actual
exercise and solution files as browser test inputs.

## Test user-visible behavior

A Playwright check can express the counter's public contract without depending
on Forma's internal signal implementation:

```js
import { test, expect } from "@playwright/test";

test("counter responds to input", async ({ page }) => {
  await page.goto("http://127.0.0.1:4174/lab/native/?count=7");
  await expect(page.locator("#native-count")).toHaveText("7");
  await page.getByRole("button", { name: "Add one", exact: true }).click();
  await expect(page.locator("#native-count")).toHaveText("8");
  await page.reload();
  await expect(page.locator("#native-count")).toHaveText("7");
});
```

This excerpt requires the native lab to be running. It checks both interaction
and the state boundary: the URL still seeds 7 after a reload, because clicking
does not save anything. Your own test should target your
application's address and behavior. Prefer a test-owned server with an isolated
port and state directory; accidentally reusing an old preview can test stale code.

## Prove native rendering without JavaScript

Request HTML using the test's HTTP client and assert that the expected value is
present. Also create a browser context with `javaScriptEnabled: false` and submit
ordinary forms. A screenshot after JavaScript runs cannot distinguish native HTML
from an empty shell filled by the browser.

For a no-JavaScript route, check for executable scripts and module preloads.
Forma's inert `application/json` state envelope is data, not executable code.
Do not write an assertion that rejects every script element regardless of type.

## Check adoption by identity

Delay the application's script response, capture an element from the server HTML,
then release the script, exercise an island and compare the element object with
the one still in the document. This detects a replacement that a text assertion
alone would miss. The native lab performs this check for its counter.

For lists, type into an uncontrolled input, reorder rows and confirm that the same
business item retains its input and DOM element. Indexed updates should change a
single row's data without losing unrelated row state.

## Test rejected input and persistence

Unit-test request parsing at boundaries, then test actual HTTP behavior: invalid
numbers, extra or duplicate values, missing records, concurrent edits and server
errors. The browser's `min`, `max` and `required` attributes improve the interface;
they do not replace server validation.

Use temporary data directories in persistence tests. Write through the application,
restart its process and read the result again. A value surviving a rerender is not
evidence of durable storage. Define conflict behavior before testing it.

For [Field Notes](/learn/full-stack/), type a draft and first assert that the
confirmed count stays unchanged. Save, wait for server acceptance, then check the
count. Verify that rejected writes preserve stored data and that **Reload
inventory** fetches current data instead of retrying a stale POST. Only a fresh
page after a process restart proves that the accepted value came back from disk.

## Keep security response checks focused

Check that separately rendered native responses carry different nonces and retain
`Cache-Control: no-store`, the enforced CSP and content-type protections. Keep
the nonce in the body paired with the response's policy. Test malformed input
through the public route, not by disabling the guard that rejects it.

The framework's release probes qualify its implementation. They do not audit your
application's authentication, authorization, storage or business rules.

## Run the same path in CI

Install from committed npm and Cargo locks, compile browser/native assets together,
run Rust unit tests, then test the application with the browser engines you support.
Keep worker counts appropriate to the runner; this site uses at most two browser
workers and one for adoption checks. Retain traces and failed assertions rather
than replacing a failure with an unexplained retry.

Automated accessibility checks complement keyboard, zoom, screen-reader and visual
review. See the [deployment guide](/docs/3.0/deployment/) for the final artifact check.
