375bcfb0fa
Copies Playwright e2e tests (58 specs, 300+ tests) designed for cross-app testing. Adapts API mocks to match real Aeroflot dictionary format (title objects with multilingual keys), adds board/schedule/days endpoint mocks, and provides Angular-specific Playwright config on port 4203.
199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('React Query Caching & Background Refresh (US-104)', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Set Russian locale
|
|
await page.addInitScript(() => {
|
|
localStorage.setItem('locale', 'ru-RU');
|
|
});
|
|
|
|
// Navigate to flight board
|
|
await page.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
|
|
|
|
// Wait for initial data load
|
|
await page.waitForLoadState('networkidle');
|
|
});
|
|
|
|
test('should display cached data immediately on initial load', async ({ page }) => {
|
|
// Check that data is displayed without spinner
|
|
const flightsList = page.locator('[data-testid="flights-list"]');
|
|
await expect(flightsList).toBeVisible();
|
|
|
|
// Should not show loading spinner on initial cached data
|
|
const spinner = page.locator('[data-testid="loading-spinner"]');
|
|
await expect(spinner).not.toBeVisible();
|
|
});
|
|
|
|
test('should refresh data in background without flickering', async ({ page }) => {
|
|
// Get initial flight count
|
|
const flights = page.locator('[data-testid="flight-item"]');
|
|
const initialCount = await flights.count();
|
|
expect(initialCount).toBeGreaterThan(0);
|
|
|
|
// Wait for background refresh (30 seconds)
|
|
await page.waitForTimeout(30_000);
|
|
|
|
// Data should still be visible (no flicker)
|
|
await expect(flights.first()).toBeVisible();
|
|
|
|
// Get new flight count (may have changed)
|
|
const newCount = await flights.count();
|
|
expect(newCount).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
test('should show stale data while refetching in background', async ({ page }) => {
|
|
// Wait for initial data load
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Get initial data
|
|
const firstFlight = page.locator('[data-testid="flight-item"]').first();
|
|
|
|
// Wait for background refetch to trigger
|
|
await page.waitForTimeout(30_000);
|
|
|
|
// Data should still be visible from cache
|
|
await expect(firstFlight).toBeVisible();
|
|
|
|
// Text should be defined (but no loading spinner)
|
|
const updatedText = await firstFlight.textContent();
|
|
expect(updatedText).toBeDefined();
|
|
});
|
|
|
|
test('should maintain cache across page navigation', async ({ page }) => {
|
|
// Verify initial data loaded
|
|
const flights = page.locator('[data-testid="flight-item"]');
|
|
const initialCount = await flights.count();
|
|
expect(initialCount).toBeGreaterThan(0);
|
|
|
|
// Navigate to flight details
|
|
await flights.first().click();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Navigate back
|
|
await page.goBack();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Cache should be used - data should appear immediately
|
|
const flightsList = page.locator('[data-testid="flights-list"]');
|
|
await expect(flightsList).toBeVisible({ timeout: 1000 });
|
|
|
|
// Should not show loading spinner (cache hit)
|
|
const spinner = page.locator('[data-testid="loading-spinner"]');
|
|
await expect(spinner).not.toBeVisible();
|
|
});
|
|
|
|
test('should clean up old cached data after 5 minutes (garbage collection)', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
// Wait for garbage collection time (5 minutes)
|
|
// In test, we'll simulate by checking cache lifecycle
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Create new page (simulates new session)
|
|
const newPage = await context.newPage();
|
|
await newPage.addInitScript(() => {
|
|
localStorage.setItem('locale', 'ru-RU');
|
|
});
|
|
|
|
// Navigate to same route
|
|
await newPage.goto('/onlineboard/departure/MSK-SPB-2026-04-09');
|
|
await newPage.waitForLoadState('networkidle');
|
|
|
|
// Should load successfully with or without cache
|
|
const flights = newPage.locator('[data-testid="flight-item"]');
|
|
await expect(flights.first()).toBeVisible();
|
|
|
|
await newPage.close();
|
|
});
|
|
|
|
test('should not break on background refetch errors', async ({ page }) => {
|
|
// Setup route to fail after initial success
|
|
let requestCount = 0;
|
|
await page.route('**/api/v1/flights', (route) => {
|
|
requestCount++;
|
|
if (requestCount === 1) {
|
|
route.continue();
|
|
} else {
|
|
// Fail subsequent requests
|
|
route.abort('failed');
|
|
}
|
|
});
|
|
|
|
// Initial data should load
|
|
await page.waitForLoadState('networkidle');
|
|
const flights = page.locator('[data-testid="flight-item"]');
|
|
const initialCount = await flights.count();
|
|
expect(initialCount).toBeGreaterThan(0);
|
|
|
|
// Wait for background refetch
|
|
await page.waitForTimeout(35_000);
|
|
|
|
// UI should still be functional (cached data still visible)
|
|
await expect(flights.first()).toBeVisible();
|
|
|
|
// No console errors (graceful failure)
|
|
const errors = (await page.evaluate(() => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return (window as any).__testErrors || [];
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
})) as any[];
|
|
expect(errors.length).toBe(0);
|
|
});
|
|
|
|
test('should respect stale time before triggering refetch', async ({ page }) => {
|
|
const apiCalls: string[] = [];
|
|
|
|
// Track all API calls
|
|
page.on('response', (response) => {
|
|
if (response.url().includes('/api/v1/flights')) {
|
|
apiCalls.push(new Date().toISOString());
|
|
}
|
|
});
|
|
|
|
// Wait for initial load
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Should have at least 1 call
|
|
expect(apiCalls.length).toBeGreaterThan(0);
|
|
|
|
// Immediately reload (within stale time)
|
|
await page.reload();
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// With 30s stale time, should use cache (no new API call within stale time)
|
|
// The reload might cause refetch, but subsequent reloads within 30s should use cache
|
|
expect(apiCalls.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
test('should display fresh data when refetch completes', async ({ page }) => {
|
|
// Get updated timestamp
|
|
const newUpdateTime = await page.locator('[data-testid="data-timestamp"]').textContent();
|
|
|
|
// Wait for background refresh
|
|
await page.waitForTimeout(35_000);
|
|
|
|
// Timestamp should be defined
|
|
expect(newUpdateTime).toBeDefined();
|
|
});
|
|
|
|
test('should handle locale switching with cache invalidation', async ({ page }) => {
|
|
// Load initial data
|
|
await page.waitForLoadState('networkidle');
|
|
const flights = page.locator('[data-testid="flight-item"]');
|
|
|
|
// Switch locale to English
|
|
const localeButton = page.locator('[data-testid="locale-switcher"]');
|
|
await localeButton.click();
|
|
const englishOption = page.locator('[data-testid="locale-en-US"]');
|
|
await englishOption.click();
|
|
|
|
// Wait for data reload with new locale
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
// Data should still be present in new locale
|
|
const newFlights = page.locator('[data-testid="flight-item"]');
|
|
await expect(newFlights.first()).toBeVisible();
|
|
});
|
|
});
|