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(); }); });