fix: ChatBubble crash and DeepSeek API compatibility

- Fix ChatBubble to handle non-string content with String() wrapper
- Fix API route to use generateText for non-streaming requests
- Add @ai-sdk/openai-compatible for non-OpenAI providers (DeepSeek, etc.)
- Use Chat Completions API instead of Responses API for compatible providers
- Update ChatBubble tests and fix component exports to kebab-case
- Remove stale PascalCase ChatBubble.tsx file
This commit is contained in:
Max
2026-01-26 16:55:05 +07:00
parent 6b113e0392
commit e9e6fadb1d
544 changed files with 113077 additions and 427 deletions

View File

@@ -0,0 +1,430 @@
# Intercept Network Call Utility
## Principle
Intercept network requests with a single declarative call that returns a Promise. Automatically parse JSON responses, support both spy (observe) and stub (mock) patterns, and use powerful glob pattern matching for URL filtering.
## Rationale
Vanilla Playwright's network interception requires multiple steps:
- `page.route()` to setup, `page.waitForResponse()` to capture
- Manual JSON parsing
- Verbose syntax for conditional handling
- Complex filter predicates
The `interceptNetworkCall` utility provides:
- **Single declarative call**: Setup and wait in one statement
- **Automatic JSON parsing**: Response pre-parsed, strongly typed
- **Flexible URL patterns**: Glob matching with picomatch
- **Spy or stub modes**: Observe real traffic or mock responses
- **Concise API**: Reduces boilerplate by 60-70%
## Pattern Examples
### Example 1: Spy on Network (Observe Real Traffic)
**Context**: Capture and inspect real API responses for validation.
**Implementation**:
```typescript
import { test } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
test('should spy on users API', async ({ page, interceptNetworkCall }) => {
// Setup interception BEFORE navigation
const usersCall = interceptNetworkCall({
url: '**/api/users', // Glob pattern
});
await page.goto('/dashboard');
// Wait for response and access parsed data
const { responseJson, status } = await usersCall;
expect(status).toBe(200);
expect(responseJson).toHaveLength(10);
expect(responseJson[0]).toHaveProperty('name');
});
```
**Key Points**:
- Intercept before navigation (critical for race-free tests)
- Returns Promise with `{ responseJson, status, requestBody }`
- Glob patterns (`**` matches any path segment)
- JSON automatically parsed
### Example 2: Stub Network (Mock Response)
**Context**: Mock API responses for testing UI behavior without backend.
**Implementation**:
```typescript
test('should stub users API', async ({ page, interceptNetworkCall }) => {
const mockUsers = [
{ id: 1, name: 'Test User 1' },
{ id: 2, name: 'Test User 2' },
];
const usersCall = interceptNetworkCall({
url: '**/api/users',
fulfillResponse: {
status: 200,
body: mockUsers,
},
});
await page.goto('/dashboard');
await usersCall;
// UI shows mocked data
await expect(page.getByText('Test User 1')).toBeVisible();
await expect(page.getByText('Test User 2')).toBeVisible();
});
```
**Key Points**:
- `fulfillResponse` mocks the API
- No backend needed
- Test UI logic in isolation
- Status code and body fully controllable
### Example 3: Conditional Response Handling
**Context**: Different responses based on request method or parameters.
**Implementation**:
```typescript
test('conditional mocking', async ({ page, interceptNetworkCall }) => {
await interceptNetworkCall({
url: '**/api/data',
handler: async (route, request) => {
if (request.method() === 'POST') {
// Mock POST success
await route.fulfill({
status: 201,
body: JSON.stringify({ id: 'new-id', success: true }),
});
} else if (request.method() === 'GET') {
// Mock GET with data
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Item' }]),
});
} else {
// Let other methods through
await route.continue();
}
},
});
await page.goto('/data-page');
});
```
**Key Points**:
- `handler` function for complex logic
- Access full `route` and `request` objects
- Can mock, continue, or abort
- Flexible for advanced scenarios
### Example 4: Error Simulation
**Context**: Testing error handling in UI when API fails.
**Implementation**:
```typescript
test('should handle API errors gracefully', async ({ page, interceptNetworkCall }) => {
// Simulate 500 error
const errorCall = interceptNetworkCall({
url: '**/api/users',
fulfillResponse: {
status: 500,
body: { error: 'Internal Server Error' },
},
});
await page.goto('/dashboard');
await errorCall;
// Verify UI shows error state
await expect(page.getByText('Failed to load users')).toBeVisible();
await expect(page.getByTestId('retry-button')).toBeVisible();
});
// Simulate network timeout
test('should handle timeout', async ({ page, interceptNetworkCall }) => {
await interceptNetworkCall({
url: '**/api/slow',
handler: async (route) => {
// Never respond - simulates timeout
await new Promise(() => {});
},
});
await page.goto('/slow-page');
// UI should show timeout error
await expect(page.getByText('Request timed out')).toBeVisible({ timeout: 10000 });
});
```
**Key Points**:
- Mock error statuses (4xx, 5xx)
- Test timeout scenarios
- Validate error UI states
- No real failures needed
### Example 5: Order Matters - Intercept Before Navigate
**Context**: The interceptor must be set up before the network request occurs.
**Implementation**:
```typescript
// INCORRECT - interceptor set up too late
await page.goto('https://example.com'); // Request already happened
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await networkCall; // Will hang indefinitely!
// CORRECT - Set up interception first
const networkCall = interceptNetworkCall({ url: '**/api/data' });
await page.goto('https://example.com');
const result = await networkCall;
```
This pattern follows the classic test spy/stub pattern:
1. Define the spy/stub (set up interception)
2. Perform the action (trigger the network request)
3. Assert on the spy/stub (await and verify the response)
### Example 6: Multiple Intercepts
**Context**: Intercepting different endpoints in same test - setup order is critical.
**Implementation**:
```typescript
test('multiple intercepts', async ({ page, interceptNetworkCall }) => {
// Setup all intercepts BEFORE navigation
const usersCall = interceptNetworkCall({ url: '**/api/users' });
const productsCall = interceptNetworkCall({ url: '**/api/products' });
const ordersCall = interceptNetworkCall({ url: '**/api/orders' });
// THEN navigate
await page.goto('/dashboard');
// Wait for all (or specific ones)
const [users, products] = await Promise.all([usersCall, productsCall]);
expect(users.responseJson).toHaveLength(10);
expect(products.responseJson).toHaveLength(50);
});
```
**Key Points**:
- Setup all intercepts before triggering actions
- Use `Promise.all()` to wait for multiple calls
- Order: intercept -> navigate -> await
- Prevents race conditions
### Example 7: Capturing Multiple Requests to the Same Endpoint
**Context**: Each `interceptNetworkCall` captures only the first matching request.
**Implementation**:
```typescript
// Capturing a known number of requests
const firstRequest = interceptNetworkCall({ url: '/api/data' });
const secondRequest = interceptNetworkCall({ url: '/api/data' });
await page.click('#load-data-button');
const firstResponse = await firstRequest;
const secondResponse = await secondRequest;
expect(firstResponse.status).toBe(200);
expect(secondResponse.status).toBe(200);
// Handling an unknown number of requests
const getDataRequestInterceptor = () =>
interceptNetworkCall({
url: '/api/data',
timeout: 1000, // Short timeout to detect when no more requests are coming
});
let currentInterceptor = getDataRequestInterceptor();
const allResponses = [];
await page.click('#load-multiple-data-button');
while (true) {
try {
const response = await currentInterceptor;
allResponses.push(response);
currentInterceptor = getDataRequestInterceptor();
} catch (error) {
// No more requests (timeout)
break;
}
}
console.log(`Captured ${allResponses.length} requests to /api/data`);
```
### Example 8: Using Timeout
**Context**: Set a timeout for waiting on a network request.
**Implementation**:
```typescript
const dataCall = interceptNetworkCall({
method: 'GET',
url: '/api/data-that-might-be-slow',
timeout: 5000, // 5 seconds timeout
});
await page.goto('/data-page');
try {
const { responseJson } = await dataCall;
console.log('Data loaded successfully:', responseJson);
} catch (error) {
if (error.message.includes('timeout')) {
console.log('Request timed out as expected');
} else {
throw error;
}
}
```
## URL Pattern Matching
The utility uses [picomatch](https://github.com/micromatch/picomatch) for powerful glob pattern matching, dramatically simplifying URL targeting:
**Supported glob patterns:**
```typescript
'**/api/users'; // Any path ending with /api/users
'/api/users'; // Exact match
'**/users/*'; // Any users sub-path
'**/api/{users,products}'; // Either users or products
'**/api/users?id=*'; // With query params
```
**Comparison with vanilla Playwright:**
```typescript
// Vanilla Playwright - complex predicate
const predicate = (response) => {
const url = response.url();
return (
url.endsWith('/api/users') ||
url.match(/\/api\/users\/\d+/) ||
(url.includes('/api/users/') && url.includes('/profile'))
);
};
page.waitForResponse(predicate);
// With interceptNetworkCall - simple glob patterns
interceptNetworkCall({ url: '/api/users' }); // Exact endpoint
interceptNetworkCall({ url: '/api/users/*' }); // User by ID pattern
interceptNetworkCall({ url: '/api/users/*/profile' }); // Specific sub-paths
interceptNetworkCall({ url: '/api/users/**' }); // Match all
```
## API Reference
### `interceptNetworkCall(options)`
| Parameter | Type | Description |
| ----------------- | ---------- | --------------------------------------------------------------------- |
| `page` | `Page` | Required when using direct import (not needed with fixture) |
| `method` | `string` | Optional: HTTP method to match (e.g., 'GET', 'POST') |
| `url` | `string` | Optional: URL pattern to match (supports glob patterns via picomatch) |
| `fulfillResponse` | `object` | Optional: Response to use when mocking |
| `handler` | `function` | Optional: Custom handler function for the route |
| `timeout` | `number` | Optional: Timeout in milliseconds for the network request |
### `fulfillResponse` Object
| Property | Type | Description |
| --------- | ------------------------ | ----------------------------------------------------- |
| `status` | `number` | HTTP status code (default: 200) |
| `headers` | `Record<string, string>` | Response headers |
| `body` | `any` | Response body (will be JSON.stringified if an object) |
### Return Value
Returns a `Promise<NetworkCallResult>` with:
| Property | Type | Description |
| -------------- | ---------- | --------------------------------------- |
| `request` | `Request` | The intercepted request |
| `response` | `Response` | The response (null if mocked) |
| `responseJson` | `any` | Parsed JSON response (if available) |
| `status` | `number` | HTTP status code |
| `requestJson` | `any` | Parsed JSON request body (if available) |
## Comparison with Vanilla Playwright
| Vanilla Playwright | intercept-network-call |
| ----------------------------------------------------------- | ------------------------------------------------------------ |
| `await page.route('/api/users', route => route.continue())` | `const call = interceptNetworkCall({ url: '**/api/users' })` |
| `const resp = await page.waitForResponse('/api/users')` | (Combined in single statement) |
| `const json = await resp.json()` | `const { responseJson } = await call` |
| `const status = resp.status()` | `const { status } = await call` |
| Complex filter predicates | Simple glob patterns |
**Reduction:** ~5-7 lines -> ~2-3 lines per interception
## Related Fragments
- `network-first.md` - Core pattern: intercept before navigate
- `network-recorder.md` - HAR-based offline testing
- `overview.md` - Fixture composition basics
## Anti-Patterns
**DON'T intercept after navigation:**
```typescript
await page.goto('/dashboard'); // Navigation starts
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // Too late!
```
**DO intercept before navigate:**
```typescript
const usersCall = interceptNetworkCall({ url: '**/api/users' }); // First
await page.goto('/dashboard'); // Then navigate
const { responseJson } = await usersCall; // Then await
```
**DON'T ignore the returned Promise:**
```typescript
interceptNetworkCall({ url: '**/api/users' }); // Not awaited!
await page.goto('/dashboard');
// No deterministic wait - race condition
```
**DO always await the intercept:**
```typescript
const usersCall = interceptNetworkCall({ url: '**/api/users' });
await page.goto('/dashboard');
await usersCall; // Deterministic wait
```