# Testing

> Test organisations, the three environments, unit and end-to-end testing with tools you already know, and the conformance suite.

Source: https://miniapp.sollar.com/guides/testing/

---

Because the runtime **is** the web platform, the testing tools you already use work unchanged. There
is no Sollar equivalent of `miniprogram-simulate` or `miniprogram-automator`, and there will not be
— Vitest, jsdom and Playwright already do the job better than a platform-specific reimplementation
would.

## Test organisations

A test organisation is a real Sollar tenant with real tokens, isolated from anything that matters.

```sh
sollar org create acme-erp-test
sollar dev --tenant acme-erp-test
```

| | |
|---|---|
| Permission and configuration changes | Take effect **immediately** — no administrator approval |
| Limit | 3 per developer |
| Production data | **Never.** Synthetic data only. |

The immediate-effect rule is what makes the loop short: changing a scope during development should
not require a person in another department.

## Three environments

| | Data | Endpoints | Permission checks | Who reaches it |
|---|---|---|---|---|
| **Development** | Synthetic | Your dev backend | Identical | You |
| **Trial** | Synthetic or staging | Staging | Identical | Named testers |
| **Production** | Real | Production | Identical | Users |

**The permission code path is identical in all three.** What changes is data and endpoints, never
the authorisation check. Environment-divergent permission behaviour is one of the six documented
vulnerability categories in this platform class, and it is designed out rather than tested for.

### Trial versions

```sh
sollar upload --trial
sollar tester add ana@acme.example
```

Only project members and explicitly named testers can reach a trial version. Testers must be members
of the organisation — the tenant is the natural boundary, so there is no separate invitation system
to administer.

Each developer holds one development version at a time; uploading replaces it. That is a working
state, not a history.

## Unit tests

```sh
npm install -D vitest jsdom
```

Mock the bridge. It is a plain object, so this is not elaborate:

```js title="test/setup.js"

globalThis.sollar = {
  version: '1.0.0',
  supports: () => true,
  app: { id: 'com.acme.erp', locale: 'en', theme: 'light' },
  identity: { get: vi.fn().mockResolvedValue({ app_user_id: 'u_test', tenant_id: 'acme-corp-test' }) },
  auth: { getToken: vi.fn().mockResolvedValue({ access_token: 'test-token', expires_in: 300 }) },
  room: { current: vi.fn().mockResolvedValue(null) },
  message: { send: vi.fn().mockResolvedValue({ event_id: '$evt' }) },
  permissions: { query: vi.fn().mockResolvedValue('granted'), request: vi.fn().mockResolvedValue('granted') },
  actions: { handle: vi.fn(), elicit: vi.fn(), progress: vi.fn() },
  ui: { toast: vi.fn(), confirm: vi.fn().mockResolvedValue(true) },
  storage: { get: vi.fn(), set: vi.fn(), remove: vi.fn(), clear: vi.fn() },
}
```

### Test the paths you hope never happen

```js
it('offers manual entry when the camera is refused', async () => {
  sollar.permissions.request.mockResolvedValue('denied')
  render(<ReceiptScanner />)
  await user.click(screen.getByRole('button', { name: /scan/i }))
  expect(screen.getByLabelText(/enter amount/i)).toBeVisible()
})

it('does not offer channel posting in an encrypted room', async () => {
  sollar.room.current.mockResolvedValue({
    room_id: '!r:sollar.com', is_encrypted: true, member_count: 8,
  })
  render(<OrderView orderId="4471" />)
  expect(screen.queryByRole('button', { name: /notify channel/i })).toBeNull()
})

it('keeps one idempotency key across retries', async () => {
  await queueApproval('4471', 'urgent')
  fetchMock.mockRejectOnce(new Error('offline'))
  await flushOutbox()
  await flushOutbox()
  const [first, second] = fetchMock.mock.calls
  expect(first[1].headers['Idempotency-Key'])
    .toBe(second[1].headers['Idempotency-Key'])
})
```

Denied permissions, encrypted rooms and retried writes are where mini apps actually break. The happy
path tends to work.

## End-to-end tests

Playwright drives `sollar dev` directly — it is a web page at a real origin.

```js title="e2e/approvals.spec.js"

test('approves an order', async ({ page }) => {
  await page.goto('https://com.acme.erp.miniapp.localhost/')
  await page.getByRole('button', { name: 'Approve 4471' }).click()
  await page.getByRole('button', { name: 'Confirm' }).click()
  await expect(page.getByText('Approved')).toBeVisible()
})
```

```js title="playwright.config.js"
export default {
  webServer: { command: 'sollar dev --tenant acme-erp-test', port: 4321 },
  use: { ignoreHTTPSErrors: true },        // the synthetic origin's local certificate
}
```

## Testing actions

An action handler is a function. Call it.

```js
it('surfaces a conflict as a sentence the agent can act on', async () => {
  fetchMock.mockResponseOnce(
    JSON.stringify({ detail: 'Purchase order 4471 was already approved on 2026-08-30 by another approver.' }),
    { status: 409 })

  await expect(handlers.approve_purchase_order({ purchase_order_id: '4471' }))
    .rejects.toThrow(/already approved on 2026-08-30/)
})
```

Assert on the *content* of the error, not just that it threw. An agent reads that sentence and
decides whether to retry; "it threw" does not tell you whether it will.

Verify your schemas match your handlers:

```js
it('every declared action has a handler', () => {
  for (const action of manifest.actions) {
    expect(handlers[action.name], `no handler for ${action.name}`).toBeTypeOf('function')
  }
})
```

`sollar validate` checks this too, but a test failure names the action.

## On device

```sh
sollar build && sollar preview
```

Prints a QR code. Scanning it in Sollar installs the build on your phone. This is where you find
what the emulator hides: how it feels on a slow network, whether the touch targets work, whether the
keyboard covers the field you need.

```sh
sollar logs --tenant acme-erp-test
```

Streams `console` output and bridge traffic from trial installs.

Chrome DevTools attaches to Android over USB; Safari Web Inspector attaches to iOS. Both work
normally, because there is nothing unusual to attach to.

## Conformance

```sh
sollar test --conformance
```

Runs Sollar's suite against the attached runtime: the ES2022 floor, the CSS baseline, every
`sollar.*` method, and every permission path. Run it against the iOS simulator and an Android
emulator before submitting — JavaScriptCore and V8 differ, and this is what surfaces the difference
before a user does.

## Continuous integration

```yaml title=".github/workflows/ci.yml"
- run: npm ci
- run: npm run test:unit
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
- run: npx sollar validate --strict
- run: npx sollar build --sbom
```

`sollar validate --strict` applies the store review checks. Running it in CI means a review failure
is a red build, not a rejected submission three days later.
