# Testing Guide

## ContainerBuilder (recommended)

`ContainerBuilder` handles `QueryClientStore` setup, store initialization, and waits for all queries to settle automatically.

```tsx
import { ContainerBuilder, waitFor } from '@servicetitan/tanstack-query-mobx/test';

describe('JobsStore', () => {
    it('should fetch jobs on initialize', async () => {
        const { container, initialize } = new ContainerBuilder()
            .add(JobsStore)
            .add(JobsApi)
            .build();

        jest.spyOn(container.get(JobsApi), 'getJobs').mockResolvedValue({
            data: [{ id: 1, title: 'Plumbing' }],
        });

        await initialize(); // calls initialize() + waits for all queries to settle

        const store = container.get(JobsStore);
        expect(store.jobs.data).toHaveLength(1);
    });
});
```

### API

| Method | Purpose |
|--------|---------|
| `.add(Token)` | Register a store or service class |
| `.add(Token, { useValue: instance })` | Register a concrete mock value |
| `.add(Token, { useClass: MockClass })` | Register a replacement class |
| `.build()` | Build the container — returns `{ container, initialize }` |
| `await initialize()` | Calls `initialize()` on all stores, waits for every `QueryApi` to reach `initialized: true` |
| `await initialize([SkipStore])` | Initialize all except listed stores |
| `await initialize({ ignoreStores, skipQueries })` | Options object form — see below |
| `new ContainerBuilder(undefined, queryClientConfig)` | Custom TanStack Query config (default: `staleTime: Infinity, retry: false`) |

**Queries that stay disabled for the duration of a test** (e.g. `enabled: false` literal, or a condition you won't flip) must be opted out explicitly via `skipQueries` or `ignoreStores` — otherwise `initialize()` will wait for them indefinitely.

### waitFor helper

Waits for a MobX observable condition to become true. Useful when you need to wait for specific state beyond initial initialization:

```tsx
await waitFor(() => store.items.initialized);
await waitFor(() => store.items.data?.length > 0);
await waitFor(() => !store.items.isFetching && store.items.isSuccess);
```

### Skipping specific queries

Use `skipQueries` to skip individual queries that you don't want to wait for (e.g., queries that depend on user interaction):

```tsx
const { container, initialize } = new ContainerBuilder()
    .add(ItemsStore)
    .add(ItemsApi)
    .build();

const store = container.get(ItemsStore);

// Don't wait for the details query to settle
await initialize({ skipQueries: [store.details] });

expect(store.items.initialized).toBe(true);
expect(store.details.initialized).toBe(false); // not fetched yet
```

You can also skip entire stores:

```tsx
await initialize({ ignoreStores: [OtherStore] });
```

### Queries that depend on external data

Queries with `enabled` tied to another store's state (e.g., `enabled: this.otherStore.selectedId > 0`) fall into two cases:

**Cascading — the dependency will flip during init:** `initialize()` waits for the whole cascade. No special handling needed; the query becomes enabled, fetches, and `initialized` flips to `true` before `initialize()` resolves.

**Gated — the dependency stays false for the lifetime of `initialize()`:** opt out with `skipQueries`, then drive the dependency from the test and wait explicitly:

```tsx
const store = container.get(ItemsStore);

await initialize({ skipQueries: [store.details] });

// Trigger the dependency
store.setSelectedId(42);
await waitFor(() => store.details.initialized);

expect(store.details.data).toBeDefined();
```

### Mock values

Pass mock objects directly instead of real classes:

```tsx
const mockApi = {
    getItems: jest.fn().mockResolvedValue({ data: [{ id: 1, name: 'Item' }] }),
    deleteItem: jest.fn().mockResolvedValue(undefined),
};

const { container, initialize } = new ContainerBuilder()
    .add(ItemsStore)
    .add(ItemsApi, { useValue: mockApi })
    .build();

await initialize();
expect(container.get(ItemsStore).items.data).toHaveLength(1);
```

### Testing mutations

```tsx
const { container, initialize } = new ContainerBuilder()
    .add(ItemsStore)
    .add(ItemsApi)
    .build();

jest.spyOn(container.get(ItemsApi), 'deleteItem').mockResolvedValue(undefined);

await initialize();
const store = container.get(ItemsStore);

await store.deleteItem.runMutation({ id: 42 });
expect(store.deleteItem.isSuccess).toBe(true);
```

### Test defaults

`ContainerBuilder` creates a `QueryClientStore` with test-friendly defaults:
- **`staleTime: Infinity`** — queries never go stale, so refetches only happen when you explicitly invalidate
- **`retry: false`** — failed queries don't retry, so test failures surface immediately
- **`globalClient` isolation** — uses the injected test client; no `window.queryClients` leak

Override these per-test if needed:

```tsx
const builder = new ContainerBuilder(undefined, {
    defaultOptions: {
        queries: { staleTime: 0, retry: 3 },
    },
});
```

### Stores with global clients

`ContainerBuilder` injects a test `QueryClientStore` that isolates from `window.queryClients`. Queries and mutations with `globalClient: 'app'` or `'page'` use that injected client — no LocalStore subclass is required for isolation.

A LocalStore that strips `globalClient` is optional cleanup if you want tests to match stores that never share a client:

```tsx
@queryStore
class JobsLocalStore extends JobsStore {
    @override
    get jobsOptions(): QueryApiOptions<Job[]> {
        return { ...super.jobsOptions, globalClient: undefined };
    }
}

new ContainerBuilder()
    .add(JobsStore, { useClass: JobsLocalStore })
    .add(JobsApi, { useValue: mockApi })
    .build();
```

If you don't need the real store logic at all, mock the shape consumers read with `useValue`:

```tsx
new ContainerBuilder()
    .add(JobsStore, { useValue: { jobs: { data: mockJobs } } })
    .build();
```

## Manual setup (without ContainerBuilder)

For full control, create a `QueryClientStore` yourself and wire up the store manually. This skips DI entirely — you `new` up the store and call `setup()` on each query.

```tsx
import { QueryClientStore } from '@servicetitan/tanstack-query-mobx';
import { when } from 'mobx';

it('should fetch items with manual setup', async () => {
    const mockApi = {
        getItems: jest.fn().mockResolvedValue({ data: [{ id: 1 }] }),
    };

    const clientStore = new QueryClientStore(undefined, {
        defaultOptions: { queries: { staleTime: Infinity, retry: false } },
    });

    // Create store directly with dependencies
    const store = new ItemsStore(mockApi);

    // Manually setup each query with the client store
    store.items.setup(clientStore);
    await when(() => store.items.initialized);

    expect(store.items.data).toHaveLength(1);

    // Clean up
    store.items.dispose();
    clientStore.dispose();
});
```

> **Note:** `ContainerBuilder` does all of the above automatically — prefer it unless you have a specific reason to wire things manually.

## Query State Reference

Each `QueryApi` instance (and `QueryApiStore`) exposes observable state that mirrors [TanStack Query's `useQuery`](https://tanstack.com/query/v5/docs/framework/react/reference/useQuery):

| Flag | When it's `true` |
|------|-------------------|
| `initialized` | After the first query attempt settles (package-specific, not in TanStack) |
| `isPending` | No settled result yet — includes disabled queries that have never run |
| `isLoading` | First fetch in flight (`isFetching && isPending`) — use for initial loading UI |
| `isFetching` | Any fetch in flight, including background refetches |
| `isRefetching` | Background refetch after the query already has data |
| `isSuccess` | Query succeeded |
| `isError` | Query failed |

**Which flag to use:**
- **Initial loading spinner**: `isLoading` (or `!initialized` for store readiness)
- **Background refresh indicator**: `isRefetching`
- **Any network activity**: `isFetching`
- **Error state**: `isError` + `error`
