Skip to content

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.

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

Terminal window
sollar org create acme-erp-test
sollar dev --tenant acme-erp-test
Permission and configuration changesTake effect immediately — no administrator approval
Limit3 per developer
Production dataNever. 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.

DataEndpointsPermission checksWho reaches it
DevelopmentSyntheticYour dev backendIdenticalYou
TrialSynthetic or stagingStagingIdenticalNamed testers
ProductionRealProductionIdenticalUsers

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.

Terminal window
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.

Terminal window
npm install -D vitest jsdom

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

test/setup.js
import { vi } from 'vitest'
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() },
}
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.

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

e2e/approvals.spec.js
import { test, expect } from '@playwright/test'
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()
})
playwright.config.js
export default {
webServer: { command: 'sollar dev --tenant acme-erp-test', port: 4321 },
use: { ignoreHTTPSErrors: true }, // the synthetic origin's local certificate
}

An action handler is a function. Call it.

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:

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.

Terminal window
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.

Terminal window
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.

Terminal window
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.

.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.