Testing

Test local changes with memory storage and a fake server response. You can test server handlers directly with a Request.

Create a client with memory storage

Use memory storage to avoid IndexedDB and browser storage in unit tests:

import { createMemoryStorageAdapter, preventRemoteWrites, valtioSync } from 'valtio-sync/client'

const sync = valtioSync({
  endpoint: '/api/sync',
  schema: { account, todos },
  storage: createMemoryStorageAdapter({ namespace: 'test' }),
  fetch: async (_input, init) => {
    const request = JSON.parse(String(init?.body))
    return Response.json({
      serverSeq: 1,
      accepted: request.ops.map((op) => ({
        mutationId: op.mutationId,
        collection: op.collection,
        id: op.id,
        serverVersion: 1,
      })),
      rejected: [],
      changes: {},
    })
  },
})

await sync.hydrate()

The example assumes you have defined account and todos as shown in the Quickstart.

Inspect pending operations

Call flush() before inspecting pending operations:

sync.todos.create({ id: 'todo_1', title: 'Draft' })
await sync.flush()

expect(sync.debug.getPendingOps()).toMatchObject([
  { collection: 'todos', type: 'create', id: 'todo_1' },
])

Use fake timers

When testing the automatic write batch with fake timers, advance the batch window before inspecting the result:

vi.useFakeTimers()

sync.todos.records.todo_1.title = 'Changed'
await vi.advanceTimersByTimeAsync(100)
await sync.flush()

Test server handlers

Call the returned server's handle method with a Request:

import { valtioSync } from 'valtio-sync/server'

const syncServer = valtioSync({
  schema: { account, todos },
  handlers: {
    todos: {
      readSnapshot: () => ({
        serverSeq: 1,
        changes: {
          upserted: [],
          deleted: [],
        },
      }),
      create: ({ record }) => ({ serverVersion: 1, record }),
    },
  },
})

const response = await syncServer.handle(
  new Request('https://app.test/api/sync', {
    method: 'POST',
    body: JSON.stringify({
      clientId: 'device_1',
      schemaVersion: 1,
      lastServerSeq: null,
      ops: [
        {
          mutationId: 'm1',
          collection: 'todos',
          type: 'create',
          id: 'todo_1',
          value: { id: 'todo_1', title: 'Local' },
          touched: ['id', 'title'],
        },
      ],
    }),
  }),
)

Use debug.getLastSyncRequest() and debug.getLastSyncResponse() for end-to-end sync assertions.

Run UI scenarios with fixture data

Keep the real client and temporarily load an isolated memory adapter.

Install preventRemoteWrites first. This lets the client fetch remote changes while keeping fixture writes away from the endpoint. Restore the default adapter before removing protection:

const removeWriteProtection = sync.interceptTransport(preventRemoteWrites)
await sync.hydrate(createMemoryStorageAdapter({ namespace: `scenario:${scenarioId}` }))

try {
  installScenarioState(sync)
} finally {
  await sync.hydrate()
  removeWriteProtection()
}

Argument-free hydrate() always reactivates the constructor-provided default adapter and reads its current contents. Scenario cleanup must await it before removing write protection.