# Saved tests

> Source: https://astrallab.dev/docs/saved-tests

A flow an agent has tested can be kept as a saved test: a script plus a few details (name, ticket, environment, credentials it uses, record mode). Any agent can replay it, you can watch it, and CI can run it.

## Where tests live

- **Project tests** are saved in `.astral/tests/<id>.astral.js` inside your project and versioned with your repository. The id is a slug of the test's name.
- **Temporary tests** are kept in Astral's data folder and expire after a number of days (7 by default).

The agent saves one with `test_save` and can link it to a ticket. A saved test replays the script, not the model, so a replay spends no model tokens and runs the same steps every time.

```js
return await astral.test('Login', async () => {
  await astral.step('Open the login page', () => astral.navigate({url: astral.url('/login')}));
  await astral.step('Sign in', async () => {
    await astral.fill({locator: {role: 'textbox', name: 'Email'}, credential: 'shop.demo', field: 'username'});
    await astral.fill({locator: {role: 'textbox', name: 'Password', exact: true}, credential: 'shop.demo', field: 'password'});
    await astral.click({locator: {role: 'button', name: 'Sign in'}});
    await astral.expect.url('/account');
  });
}, {record: 'actions'});
```

## .astral/config.json

The project's `.astral/config.json` declares the environments your tests run against and the credentials they may use, **by name**. Tests navigate with `astral.url('/login')`, so the same test runs against local, staging or production.

```json
{
  "project": "shop",
  "envs": {
    "local":   { "baseUrl": "http://localhost:3000" },
    "staging": { "baseUrl": "https://staging.example.com" },
    "prod":    { "baseUrl": "https://www.example.com", "protected": true }
  },
  "defaultEnv": "local",
  "collections": {
    "smoke": { "name": "Smoke", "tests": ["login", "checkout"] }
  },
  "credentials": {
    "shop.demo": {
      "username": "demo@example.com",
      "password": { "keychain": "shop.demo.password" }
    }
  }
}
```

| Key | What it is |
| --- | --- |
| `envs` | Named base URLs. `protected: true` marks an environment such as production. No user name or password in the URL. |
| `defaultEnv` | The environment used when none is given. |
| `collections` | Tests run in order as one group, with a pass or fail summary and a report linking each test's. |
| `credentials` | Named logins: a username, a password, a one-time-code seed and extra fields. Secrets are references, never values. |
| `layout` | Selectors the layout check ignores and its thresholds. |

## Credentials and the keychain

A secret in `config.json` is a reference to where it lives, never the value. A plain-text password in the file is rejected. A reference is one of:

```
{ "keychain": "<id>" }
{ "env": "ASTRAL_TEST_<NAME>" }
{ "local": "<key>" }
```

| Reference | Where the value is |
| --- | --- |
| `keychain` | Your system keychain. You set it in Astral; an agent can never read it back. |
| `env` | An environment variable whose name starts with `ASTRAL_TEST_`. |
| `local` | `.astral/secrets.local.json`, which must be in `.gitignore`. Astral refuses to create it otherwise and warns if it already exists unignored. |

- The agent only ever sees the credential's name and username. `astral.fill` resolves the value itself and types it with real key strokes; the agent only learns that it was typed and masked.
- A secret is typed only into a sensitive field (password, one-time code, card) on the environment's own origin, so it cannot be read back or sent to another site. Reports show •••••• in its place.
- A keychain secret is stored together with the environments' origins at that moment and only typed into those, even if `config.json` is edited later. For sensitive logins, prefer the keychain.

## Protected environments

On an environment marked `protected` (production, say), only tests declared `readOnly` run, and only after a person approves that exact version of the test in Astral. An agent can ask for `readOnly: true`, but that is a request, not an approval: you approve it with **Approve as read-only** in the Tests panel, the approval is kept outside the project, and it is lost if the test is edited. In a collection, tests that are not approved are skipped with the reason.

## Viewports

A test or a collection can run at one or several viewports, and Astral runs every test at each one.

| Preset | Size (CSS px) | Pixel ratio | Touch |
| --- | --- | --- | --- |
| `mobile` | 390 × 844 | 3   | yes |
| `tablet` | 820 × 1180 | 2   | yes |
| `desktop` | 1440 × 900 | 1   | no  |

A custom viewport has a width and a height from 200 to 4096 px, and optionally a pixel ratio, touch, a user agent and a name. Inside Astral the tab's panel is resized to the viewport for the run and given back its own size afterwards; pixel ratio, touch and user agent are not emulated there, and the run says so. The headless runner with Chrome emulates all of them.

## The layout check

The layout check looks at the page as it is rendered and reports findings, each with a rule, a severity, a selector, a rectangle, a message and a suggested CSS fix.

| Rule | What it finds |
| --- | --- |
| `horizontal-overflow` | The page is wider than the viewport; it names the element that escapes. |
| `overlap` | Visible text or controls on top of each other. |
| `text-clipped` | Text cut by overflow, or a long word spilling out of its box. |
| `offscreen` | A control outside the horizontal viewport. |
| `tap-target` | A touch target under 24 × 24 px is an error; under 44 × 44, a warning. Touch viewports only. |
| `small-text` | Text under 12 px on screens up to 600 px wide. |

Three ways to use it:

- The [`layout_check`](https://astrallab.dev/docs/tools.md#tool-layout_check) tool: the agent finds a problem, fixes the CSS and checks again.
- `astral.expect.layout()` as a step of a test.
- `layoutCheck: true` on a test or collection, to add a check at the end of each viewport run.

```json
{
  "collections": {
    "rwd": { "name": "Responsive", "tests": ["home", "checkout"], "viewports": ["mobile", "tablet", "desktop"], "layoutCheck": true }
  },
  "layout": { "ignore": [".chat-widget", "#cookie-banner"] }
}
```

With a screenshot, the page is captured with every finding outlined. On an engine that does not lay pages out the check is reported as skipped, never as passed.

## Replay with Play

The agent calls `test_open_in_astral` and Astral opens its Tests panel with the test loaded and paused. You press **Play** to watch it run: slow motion, pause, step by step and stop. By default the agent cannot start it: Play is also a safety control. For demos and reviews you asked it to drive, the agent can start it with `play: true`; that needs the same permission as `test_run`, works only for tests saved in a project you have allowed, and you keep Pause, Step and Stop.
