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
Section titled “Test organisations”A test organisation is a real Sollar tenant with real tokens, isolated from anything that matters.
sollar org create acme-erp-testsollar 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
Section titled “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
Section titled “Trial versions”sollar upload --trialsollar tester add ana@acme.exampleOnly 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
Section titled “Unit tests”npm install -D vitest jsdomMock the bridge. It is a plain object, so this is not elaborate:
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() },}Test the paths you hope never happen
Section titled “Test the paths you hope never happen”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
Section titled “End-to-end tests”Playwright drives sollar dev directly — it is a web page at a real origin.
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()})export default { webServer: { command: 'sollar dev --tenant acme-erp-test', port: 4321 }, use: { ignoreHTTPSErrors: true }, // the synthetic origin's local certificate}Testing actions
Section titled “Testing actions”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.
On device
Section titled “On device”sollar build && sollar previewPrints 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.
sollar logs --tenant acme-erp-testStreams 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
Section titled “Conformance”sollar test --conformanceRuns 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
Section titled “Continuous integration”- 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 --sbomsollar 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.