fix: unify theme system - fix theme switching persistence

- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
This commit is contained in:
2026-01-18 22:33:41 +01:00
parent ef60dafd73
commit ddb67ba9e5
306 changed files with 59580 additions and 6063 deletions

View File

@@ -2,7 +2,7 @@
## Principle
Use Cypress-style polling with Playwright's `expect.poll` to wait for asynchronous conditions. Provides configurable timeout, interval, logging, and post-polling callbacks with enhanced error categorization.
Use Cypress-style polling with Playwright's `expect.poll` to wait for asynchronous conditions. Provides configurable timeout, interval, logging, and post-polling callbacks with enhanced error categorization. **Ideal for backend testing**: polling API endpoints for job completion, database eventual consistency, message queue processing, and cache propagation.
## Rationale
@@ -21,6 +21,29 @@ The `recurse` utility provides:
- **Post-poll callbacks**: Process results after success
- **Type-safe**: Full TypeScript generic support
## Quick Start
```typescript
import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
test('wait for job completion', async ({ recurse, apiRequest }) => {
const { body } = await apiRequest({
method: 'POST',
path: '/api/jobs',
body: { type: 'export' },
});
// Poll until job completes
const result = await recurse(
() => apiRequest({ method: 'GET', path: `/api/jobs/${body.id}` }),
(response) => response.body.status === 'completed',
{ timeout: 60000 }
);
expect(result.body.downloadUrl).toBeDefined();
});
```
## Pattern Examples
### Example 1: Basic Polling
@@ -48,7 +71,7 @@ test('should wait for job completion', async ({ recurse, apiRequest }) => {
timeout: 60000, // 60 seconds max
interval: 2000, // Check every 2 seconds
log: 'Waiting for export job to complete',
},
}
);
expect(result.body.downloadUrl).toBeDefined();
@@ -62,7 +85,7 @@ test('should wait for job completion', async ({ recurse, apiRequest }) => {
- Options: timeout, interval, log message
- Returns the value when predicate returns true
### Example 2: Polling with Assertions
### Example 2: Working with Assertions
**Context**: Use assertions directly in predicate for more expressive tests.
@@ -76,35 +99,76 @@ test('should poll with assertions', async ({ recurse, apiRequest }) => {
body: { type: 'user-created', userId: '123' },
});
// Poll with assertions in predicate
// Poll with assertions in predicate - no return true needed!
await recurse(
async () => {
const { body } = await apiRequest({ method: 'GET', path: '/api/events/123' });
return body;
},
(event) => {
// Use assertions instead of boolean returns
// If all assertions pass, predicate succeeds
expect(event.processed).toBe(true);
expect(event.timestamp).toBeDefined();
// If assertions pass, predicate succeeds
// No need to return true - just let assertions pass
},
{ timeout: 30000 },
{ timeout: 30000 }
);
});
```
**Key Points**:
**Why no `return true` needed?**
- Predicate can use `expect()` assertions
- If assertions throw, polling continues
- If assertions pass, polling succeeds
- More expressive than boolean returns
The predicate checks for "truthiness" of the return value. But there's a catch - in JavaScript, an empty `return` (or no return) returns `undefined`, which is falsy!
### Example 3: Custom Error Messages
The utility handles this by checking if:
**Context**: Provide context-specific error messages for timeout failures.
1. The predicate didn't throw (assertions passed)
2. The return value was either `undefined` (implicit return) or truthy
**Implementation**:
So you can:
```typescript
// Option 1: Use assertions only (recommended)
(event) => {
expect(event.processed).toBe(true);
};
// Option 2: Return boolean (also works)
(event) => event.processed === true;
// Option 3: Mixed (assertions + explicit return)
(event) => {
expect(event.processed).toBe(true);
return true;
};
```
### Example 3: Error Handling
**Context**: Understanding the different error types.
**Error Types:**
```typescript
// RecurseTimeoutError - Predicate never returned true within timeout
// Contains last command value and predicate error
try {
await recurse(/* ... */);
} catch (error) {
if (error instanceof RecurseTimeoutError) {
console.log('Timed out. Last value:', error.lastCommandValue);
console.log('Last predicate error:', error.lastPredicateError);
}
}
// RecurseCommandError - Command function threw an error
// The command itself failed (e.g., network error, API error)
// RecursePredicateError - Predicate function threw (not from assertions failing)
// Logic error in your predicate code
```
**Custom Error Messages:**
```typescript
test('custom error on timeout', async ({ recurse, apiRequest }) => {
@@ -115,7 +179,7 @@ test('custom error on timeout', async ({ recurse, apiRequest }) => {
{
timeout: 10000,
error: 'System failed to become ready within 10 seconds - check background workers',
},
}
);
} catch (error) {
// Error message includes custom context
@@ -125,13 +189,6 @@ test('custom error on timeout', async ({ recurse, apiRequest }) => {
});
```
**Key Points**:
- `error` option provides custom message
- Replaces default "Timed out after X ms"
- Include debugging hints in error message
- Helps diagnose failures faster
### Example 4: Post-Polling Callback
**Context**: Process or log results after successful polling.
@@ -151,7 +208,7 @@ test('post-poll processing', async ({ recurse, apiRequest }) => {
console.log(`Processed ${result.body.itemsProcessed} items`);
return result.body;
},
},
}
);
expect(finalResult.itemsProcessed).toBeGreaterThan(0);
@@ -165,7 +222,67 @@ test('post-poll processing', async ({ recurse, apiRequest }) => {
- Can transform or log results
- Return value becomes final `recurse` result
### Example 5: Integration with API Request (Common Pattern)
### Example 5: UI Testing Scenarios
**Context**: Wait for UI elements to reach a specific state through polling.
**Implementation**:
```typescript
test('table data loads', async ({ page, recurse }) => {
await page.goto('/reports');
// Poll for table rows to appear
await recurse(
async () => page.locator('table tbody tr').count(),
(count) => count >= 10, // Wait for at least 10 rows
{
timeout: 15000,
interval: 500,
log: 'Waiting for table data to load',
}
);
// Now safe to interact with table
await page.locator('table tbody tr').first().click();
});
```
### Example 6: Event-Based Systems (Kafka/Message Queues)
**Context**: Testing eventual consistency with message queue processing.
**Implementation**:
```typescript
test('kafka event processed', async ({ recurse, apiRequest }) => {
// Trigger action that publishes Kafka event
await apiRequest({
method: 'POST',
path: '/api/orders',
body: { productId: 'ABC123', quantity: 2 },
});
// Poll for downstream effect of Kafka consumer processing
const inventoryResult = await recurse(
() => apiRequest({ method: 'GET', path: '/api/inventory/ABC123' }),
(res) => {
// Assumes test fixture seeds inventory at 100; in production tests,
// fetch baseline first and assert: expect(res.body.available).toBe(baseline - 2)
expect(res.body.available).toBeLessThanOrEqual(98);
},
{
timeout: 30000, // Kafka processing may take time
interval: 1000,
log: 'Waiting for Kafka event to be processed',
}
);
expect(inventoryResult.body.lastOrderId).toBeDefined();
});
```
### Example 7: Integration with API Request (Common Pattern)
**Context**: Most common use case - polling API endpoints for state changes.
@@ -193,7 +310,7 @@ test('end-to-end polling', async ({ apiRequest, recurse }) => {
timeout: 120000, // 2 minutes for large imports
interval: 5000, // Check every 5 seconds
log: `Polling import ${createResp.importId}`,
},
}
);
expect(importResult.body.rowsImported).toBeGreaterThan(1000);
@@ -208,20 +325,26 @@ test('end-to-end polling', async ({ apiRequest, recurse }) => {
- Complex predicates with multiple conditions
- Logging shows polling progress in test reports
## Enhanced Error Types
## API Reference
The utility categorizes errors for easier debugging:
### RecurseOptions
```typescript
// TimeoutError - Predicate never returned true
Error: Polling timed out after 30000ms: Job never completed
| Option | Type | Default | Description |
| ---------- | ------------------ | ----------- | ------------------------------------ |
| `timeout` | `number` | `30000` | Maximum time to wait (ms) |
| `interval` | `number` | `1000` | Time between polls (ms) |
| `log` | `string` | `undefined` | Message logged on each poll |
| `error` | `string` | `undefined` | Custom error message for timeout |
| `post` | `(result: T) => R` | `undefined` | Callback after successful poll |
| `delay` | `number` | `0` | Initial delay before first poll (ms) |
// CommandError - Command function threw
Error: Command failed: Request failed with status 500
### Error Types
// PredicateError - Predicate function threw (not from assertions)
Error: Predicate failed: Cannot read property 'status' of undefined
```
| Error Type | When Thrown | Properties |
| ----------------------- | --------------------------------------- | ---------------------------------------- |
| `RecurseTimeoutError` | Predicate never passed within timeout | `lastCommandValue`, `lastPredicateError` |
| `RecurseCommandError` | Command function threw an error | `cause` (original error) |
| `RecursePredicateError` | Predicate threw (not assertion failure) | `cause` (original error) |
## Comparison with Vanilla Playwright
@@ -236,11 +359,11 @@ Error: Predicate failed: Cannot read property 'status' of undefined
**Use recurse for:**
- Background job completion
- Webhook/event processing
- Database eventual consistency
- Cache propagation
- State machine transitions
- Background job completion
- Webhook/event processing
- Database eventual consistency
- Cache propagation
- State machine transitions
**Stick with vanilla expect.poll for:**
@@ -250,13 +373,15 @@ Error: Predicate failed: Cannot read property 'status' of undefined
## Related Fragments
- `api-testing-patterns.md` - Comprehensive pure API testing patterns
- `api-request.md` - Combine for API endpoint polling
- `overview.md` - Fixture composition patterns
- `fixtures-composition.md` - Using with mergeTests
- `contract-testing.md` - Contract testing with async verification
## Anti-Patterns
**❌ Using hard waits instead of polling:**
**DON'T use hard waits instead of polling:**
```typescript
await page.click('#export');
@@ -264,33 +389,33 @@ await page.waitForTimeout(5000); // Arbitrary wait
expect(await page.textContent('#status')).toBe('Ready');
```
**✅ Poll for actual condition:**
**DO poll for actual condition:**
```typescript
await page.click('#export');
await recurse(
() => page.textContent('#status'),
(status) => status === 'Ready',
{ timeout: 10000 },
{ timeout: 10000 }
);
```
**❌ Polling too frequently:**
**DON'T poll too frequently:**
```typescript
await recurse(
() => apiRequest({ method: 'GET', path: '/status' }),
(res) => res.body.ready,
{ interval: 100 }, // Hammers API every 100ms!
{ interval: 100 } // Hammers API every 100ms!
);
```
**✅ Reasonable interval for API calls:**
**DO use reasonable interval for API calls:**
```typescript
await recurse(
() => apiRequest({ method: 'GET', path: '/status' }),
(res) => res.body.ready,
{ interval: 2000 }, // Check every 2 seconds (reasonable)
{ interval: 2000 } // Check every 2 seconds (reasonable)
);
```