# BFUT: Big Friggin Unit Tests

A **Big Friggin Unit Test (BFUT)** is a testing philosophy in Maddox that focuses on validating the **API-UI Contract**. Instead of testing components in isolation with mocked hooks, or testing loaders in isolation with raw requests, a BFUT runs the real integration between your React Router v7 / Remix loaders, actions, and components.

The goal is to ensure that the data structure generated by your server logic perfectly aligns with what your UI expects to render, across all possible states.

---

## 1. API-UI Contract Validation (Basic Route)

**Scenario**: You have a route with a loader that fetches data and a component that renders it.
**Goal**: Ensure the data returned by the loader correctly populates the UI.

```javascript
import { RemixScenario } from 'maddox';
import * as ProfileRoute from './routes/profile';

new RemixScenario(this)
  .addStub({ mockName: 'Profile', path: '/profile/:id', module: ProfileRoute })
  .withInitialEntries(['/profile/123'])
  .triggerRender()
  .next(async ({ screen }) => {
    // Verify the UI contract: Does the loader data render where we expect?
    await screen.findByText('User: Ada Lovelace');
    await screen.findByText('Role: Admin');
  })
  .test((err, response) => {
    Maddox.compare.equal(response[0].mockName, 'Profile');
    Maddox.compare.equal(response[0].kind, 'loader');
  });
```

---

## 2. UI State Variations (Data-Driven UI)

**Scenario**: Testing how the UI handles different data states (empty, partial, or error).
**Goal**: Verify that "No Results", "Loading", or "Error" states render correctly based on the API response.

```javascript
// Test Case: Empty State
new RemixScenario(this)
  .addStub({ mockName: 'List', path: '/items', module: ListRoute })
  .mockThisFunction('ItemProxy', 'getAll', itemProxy)
  .shouldBeCalledWith('ItemProxy', 'getAll', [])
  .doesReturnWithPromise('ItemProxy', 'getAll', []) // Return empty array
  .triggerRender()
  .next(async ({ screen }) => {
    await screen.findByText('No items found. Please add one!');
  })
  .test();

// Test Case: Error State
new RemixScenario(this)
  .addStub({ mockName: 'List', path: '/items', module: ListRoute })
  .mockThisFunction('ItemProxy', 'getAll', itemProxy)
  .doesErrorWithPromise('ItemProxy', 'getAll', new Error('DB Down'))
  .triggerRender()
  .next(async ({ screen }) => {
    // Verifies the ErrorBoundary renders
    await screen.findByText('Something went wrong: DB Down');
  })
  .test();
```

---

## 3. Full Round Trip (Loader + Action)

**Scenario**: A component that loads data and then performs an action (e.g., a form submission).
**Goal**: Test the complete cycle: Initial Load → User Interaction → Action Execution → Revalidation → UI Update.

```javascript
new RemixScenario(this)
  .addStub({ mockName: 'Settings', path: '/settings', module: SettingsRoute })
  .triggerRender()
  .next(async ({ screen, userEvent }) => {
    const input = screen.getByLabelText('Display Name');
    await userEvent.clear(input);
    await userEvent.type(input, 'New Name');
    await userEvent.click(screen.getByText('Save'));

    // Revalidation: Maddox ensures the loader runs again after the action
    await screen.findByText('Settings saved successfully!');
    await screen.findByText('Current Name: New Name');
  })
  .test((err, response) => {
    // response will contain [Loader, Action, Loader (revalidation)]
    Maddox.compare.equal(response.length, 3);
    Maddox.compare.equal(response[1].kind, 'action');
  });
```

---

## 4. Complex Server Logic Validation

**Scenario**: Loaders or actions with intricate business logic (permissions, transformations).
**Goal**: Ensure complex server-side calculations result in the correct UI state.

```javascript
new RemixScenario(this)
  .addStub({ mockName: 'Pricing', path: '/pricing', module: PricingRoute })
  .withStubAppContext({ session: { userRole: 'VIP' } }) // Inject context
  .triggerRender()
  .next(async ({ screen }) => {
    // Verify that the VIP discount logic in the loader/action 
    // correctly reflects in the UI component.
    await screen.findByText('Your VIP Price: $80.00 (20% off)');
  })
  .test();
```

---

## 5. Distributed Actions & Resource Routes

**Scenario**: A component that triggers actions on different routes or resource routes.
**Goal**: Verify the component interacts correctly with various endpoints.

```javascript
new RemixScenario(this)
  .addStub({ mockName: 'Dashboard', path: '/dashboard', module: DashboardRoute })
  .addStub({ mockName: 'DeleteAction', path: '/api/delete', module: DeleteResourceRoute })
  .triggerRender()
  .next(async ({ screen, userEvent }) => {
    await userEvent.click(screen.getByTestId('delete-btn-1'));
    
    // Verify the dashboard UI updates (revalidates) after the resource route action
    await screen.findByText('Item deleted.');
  })
  .test((err, response) => {
    const deleteAction = response.find(r => r.mockName === 'DeleteAction');
    Maddox.compare.truthy(deleteAction);
  });
```

---

## 6. Testing Resource Routes (API Actions)

**Scenario**: You have a route that only exports an `action` (no component) and you want to test its logic in isolation.
**Goal**: Use targeted `triggerAction` to directly invoke the action with different payload formats (`encType`s) without manually adding a stub.

### A. Default Form-Data Submission (`application/x-www-form-urlencoded`)

By default, React Router and Maddox treat programmatic submissions as standard URL-encoded form data. When you pass a plain JavaScript object as the `body`, it is serialized and must be retrieved using `request.formData()` in your action.

```javascript
import { RemixScenario } from 'maddox';
import * as LogoutAction from './routes/api.logout';

new RemixScenario(this)
  .triggerAction(
    { mockName: 'Logout', path: '/api/logout', module: LogoutAction },
    { 
      method: 'post', 
      body: { reason: 'user_clicked_logout' } 
    }
  )
  .test((err, response) => {
    const logout = response.find(r => r.mockName === 'Logout');
    Maddox.compare.equal(logout.kind, 'action');
    // Verify redirect or other action results
    Maddox.compare.equal(logout.value.status, 302);
  });
```

*In your action:*
```javascript
export async function action({ request }) {
  const formData = await request.formData();
  const reason = formData.get('reason'); // "user_clicked_logout"
  // ... perform logout ...
}
```

### B. JSON Payload Submission (`application/json`)

If your resource route behaves like a modern REST API endpoint and expects JSON payload parsing via `request.json()`, you must explicitly supply `encType: 'application/json'`. React Router will then serialize the `body` and set the `Content-Type` header accordingly, preserving raw JavaScript types (like booleans, numbers, nested objects, or arrays).

```javascript
import { RemixScenario } from 'maddox';
import * as SaveSettingsAction from './routes/api.settings';

new RemixScenario(this)
  .triggerAction(
    { mockName: 'SaveSettings', path: '/api/settings', module: SaveSettingsAction },
    { 
      method: 'put',
      encType: 'application/json',
      body: { 
        notificationsEnabled: true, // Remains a boolean
        theme: 'dark',
        categories: ['tech', 'news'] // Remains an array
      } 
    }
  )
  .test((err, response) => {
    const settings = response.find(r => r.mockName === 'SaveSettings');
    Maddox.compare.equal(settings.kind, 'action');
    Maddox.compare.equal(settings.value.success, true);
  });
```

*In your action:*
```javascript
export async function action({ request }) {
  const payload = await request.json();
  const notificationsEnabled = payload.notificationsEnabled; // true (boolean)
  const theme = payload.theme; // "dark"
  const categories = payload.categories; // ["tech", "news"]
  // ... save settings ...
}
```

---

## 7. Testing Dynamic Resource Routes

**Scenario**: Testing a loader or action that depends on URL parameters (e.g., `/api/users/123`).
**Goal**: Use `triggerLoader` or `triggerAction` with an explicit `url` to resolve dynamic segments.

```javascript
import { RemixScenario } from 'maddox';
import * as UserLoader from './routes/api.user';

new RemixScenario(this)
  .triggerLoader(
    { mockName: 'User', path: '/api/users/:id', module: UserLoader }, 
    { url: '/api/users/123' } // Resolve the :id segment
  )
  .test((err, response) => {
    const user = response.find(r => r.mockName === 'User');
    Maddox.compare.equal(user.value.id, '123');
  });
```

---

## 8. Nested Routes (Parent/Child Loaders)

**Scenario**: You have a parent layout route and a nested child route, both with their own loaders.
**Goal**: Verify that both loaders execute and their data is available to the UI.

```javascript
import { RemixScenario } from 'maddox';
import { Outlet } from 'react-router';
import * as ParentRoute from './routes/parent';
import * as ChildRoute from './routes/child';

new RemixScenario(this)
  .addStub({ 
    mockName: 'Parent', 
    path: '/parent', 
    module: ParentRoute,
    children: [
      { mockName: 'Child', path: 'child', module: ChildRoute }
    ]
  })
  .withInitialEntries(['/parent/child'])
  .triggerRender()
  .next(async ({ screen }) => {
    // Both loaders have run, and both components are rendered
    await screen.findByText('Parent Layout');
    await screen.findByText('Child Content');
  })
  .test((err, response) => {
    // response contains entries for both loaders
    Maddox.compare.truthy(response.find(r => r.mockName === 'Parent'));
    Maddox.compare.truthy(response.find(r => r.mockName === 'Child'));
  });
```

> **Note**: If you have a nested stub structure, you must navigate to the leaf path (the child's path) in `initialEntries` if you want both the parent and child loaders to execute.

---

## `withInitialEntries` vs. Default Navigation

Maddox allows you to control the starting URL of your test. Understanding when to use `withInitialEntries` is key to robust BFUTs.

### When to use `withInitialEntries`
*   **Path Parameters**: When your route depends on `:id` or other segments (e.g., `/users/123`).
*   **Query Parameters**: When your loader/action uses `request.url` to parse search params (e.g., `/search?q=maddox`).
*   **Revalidation/Navigation Testing**: When you want to verify that an action on one page correctly triggers a loader on another, or redirects the user to a specific destination.

```javascript
// Example: Testing a search page with query params
new RemixScenario(this)
  .addStub({ mockName: 'Search', path: '/search', module: SearchRoute })
  .withInitialEntries(['/search?q=maddox']) // Essential for the loader to see the query
  .triggerRender()
  .next(async ({ screen }) => {
    await screen.findByText('Results for "maddox"');
  })
  .test();
```

### When you DON'T need `withInitialEntries`
*   **Index Routes**: If your test targets the root path `/` and doesn't require specific parameters.
*   **Isolated Action Testing**: If you are using `triggerAction` and the action logic doesn't depend on the current URL (though `triggerAction` still works fine with it).
*   **Simple Component Rendering**: When you just want to see if the component mounts with default data.

```javascript
// Example: Simple index page test
new RemixScenario(this)
  .addStub({ mockName: 'Home', path: '/', module: HomeRoute })
  // No withInitialEntries needed; defaults to ['/']
  .triggerRender()
  .next(async ({ screen }) => {
    await screen.findByText('Welcome to the Dashboard');
  })
  .test();
```

---

## `withStubAppContext` vs. `withRequestMiddleware`

Both functions allow you to inject data into the `{ request, params, context }` object passed to your loaders and actions, but they serve different purposes in a BFUT.

### `withStubAppContext` (Static Context)

Use this to provide the **initial server context** that would normally be set up once when your server starts or per-request in a `getLoadContext` function. This is ideal for dependency injection of services, database connections, or static session data.

```javascript
// Example: Injecting a service and a static session
new RemixScenario(this)
  .addStub({ mockName: 'Profile', path: '/profile', module: ProfileRoute })
  .withStubAppContext({ 
    userService: new MockUserService(),
    session: { userId: 'user_123' } 
  })
  .triggerRender()
  .next(async ({ screen }) => {
    // The loader can now access context.userService and context.session
    await screen.findByText('Welcome, user_123');
  })
  .test();
```

### `withRequestMiddleware` (Dynamic Transformation)

Use this to simulate **procedural logic** that happens before your loader/action runs. This is ideal for logic that depends on the incoming `Request` (like parsing cookies or headers) and needs to dynamically modify the `context` or `request` object.

```javascript
// Example: Simulating an Auth Middleware that parses a cookie
new RemixScenario(this)
  .addStub({ mockName: 'Dashboard', path: '/dashboard', module: DashboardRoute })
  .withRequestMiddleware(async ({ request, params, context }) => {
    const cookie = request.headers.get('Cookie');
    if (cookie?.includes('session=valid')) {
      context.user = { id: 'user_456', name: 'Bob' };
    }
    return { request, params, context };
  })
  .triggerRender()
  .next(async ({ screen }) => {
    // The loader receives a context that was dynamically built by the middleware
    await screen.findByText('Hello, Bob');
  })
  .test();
```

### Summary Comparison

| Feature | `withStubAppContext` | `withRequestMiddleware` |
| :--- | :--- | :--- |
| **Primary Use** | Static Dependency Injection | Dynamic Request/Context Transformation |
| **Analogy** | `getLoadContext` in Remix | Express/Koa Middleware |
| **Input** | A static object | A function receiving `{ request, params, context }` |
| **Timing** | Set once during scenario setup | Runs before every loader/action invocation |

---

## Currently Unsupported (The "Gaps")

Maddox is actively evolving. The following React Router v7 / Remix features are not yet natively supported in `RemixScenario`:

*   **Client-side Handlers**: `clientLoader` and `clientAction` are not yet intercepted by the capture pipeline.
*   **Deferred Data**: No explicit support for `defer()`, `Suspense`, and the `<Await>` component.
*   **Streaming**: Support for streaming responses (e.g., `renderToReadableStream`) is not yet implemented.
*   **Complex File Uploads**: Handling `multipart/form-data` with `File` or `Blob` objects in `triggerAction` is limited by JSDOM.
*   **Specialized Cookie Helpers**: No built-in helpers for cookie parsing/signing; must be handled via `withRequestMiddleware`.
*   **Concurrency/Timing Assertions**: No API to assert on the exact timing or parallel execution order of loaders.
