Files
flights_web/tests/e2e-angular/cross-app/14-search-history.spec.ts
T
gnezim 375bcfb0fa Add e2e test suite from flights-front with Angular API mocks
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.
2026-04-15 23:07:44 +03:00

766 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '../support/cross-app-fixtures';
import { mockAllAPIs } from '../support/cross-app-fixtures';
import { S, tid } from '../support/selectors';
test.describe('Search History (Cross-App)', () => {
test.beforeEach(async ({ page, localePath }) => {
await mockAllAPIs(page);
// API mocks are applied globally via fixture
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
});
// Test 316-319: Search History Section Display
test('316: Search history section is visible on landing page when there is history', async ({
page,
app,
locale,
localePath,
}) => {
// Perform a search first to populate history
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing page
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Verify search history section exists
const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
const count = await historySection.count();
if (count === 0) {
test.skip(true, 'Search history section not implemented');
return;
}
await expect(historySection).toBeVisible({ timeout: 5000 });
});
test('317: Search history section has correct heading', async ({
page,
app,
locale,
localePath,
}) => {
// Perform a search to populate history
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
const count = await historySection.count();
if (count === 0) {
test.skip(true, 'Search history section not implemented');
return;
}
// Check for heading in the section
const heading = historySection.locator('h3, h2, [class*="title"]').first();
await expect(heading).toBeVisible();
const text = await heading.textContent();
expect(text?.trim().length).toBeGreaterThan(0);
// Should contain text about history (in locale-appropriate language)
if (locale === 'ru-ru') {
expect(text?.toLowerCase()).toContain('история');
}
});
test('318: Empty state message shows when no history exists', async ({
page,
app,
localePath,
}) => {
// Clear localStorage to ensure no history
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
const count = await historySection.count();
// Section might be hidden entirely when empty, which is acceptable
if (count === 0) {
// This is acceptable behavior - section hidden when empty
expect(count).toBe(0);
return;
}
// If section exists, it should show either empty message or no items
const items = historySection.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
const itemCount = await items.count();
// Either section is hidden or items list is empty
if (itemCount === 0) {
const emptyMessage = historySection.locator('text=/No|empty|История|пусто/i');
const emptyCount = await emptyMessage.count();
// If items are empty, should have some empty state indicator (optional)
// Just verify section doesn't crash
await expect(historySection).toBeVisible();
}
});
test('319: Search history items appear after performing searches', async ({
page,
app,
locale,
localePath,
}) => {
// Clear history first
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
// Perform first search
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Perform second search
await page.goto(localePath(`onlineboard/departure/LED-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historySection = page.locator(tid(S.LANDING_SEARCH_HISTORY, app));
const sectionCount = await historySection.count();
if (sectionCount === 0) {
test.skip(true, 'Search history not implemented');
return;
}
// Should have history items visible
const items = historySection.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
const itemCount = await items.count();
expect(itemCount).toBeGreaterThan(0);
});
// Test 320-323: Search History Item Display
test('320: Search history item shows search parameters or label', async ({
page,
app,
locale,
localePath,
}) => {
// Perform a search
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
// Item should have visible text (the search label)
const text = await historyItem.textContent();
expect(text?.trim().length).toBeGreaterThan(0);
// Should contain search info
expect(text).toMatch(/MOW|мос/i);
});
test('321: Search history item is clickable and navigates', async ({
page,
app,
locale,
localePath,
}) => {
// Perform a search
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
// Click the item - find the link inside
const link = historyItem.locator('a').first();
const linkCount = await link.count();
if (linkCount === 0) {
test.skip(true, 'Search history item is not a link');
return;
}
const urlBefore = page.url();
await link.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const urlAfter = page.url();
// Should navigate to search results page
expect(urlAfter).not.toBe(urlBefore);
expect(urlAfter).toMatch(/onlineboard/);
});
test('322: Multiple search history items are ordered (most recent first)', async ({
page,
app,
locale,
localePath,
}) => {
// Clear history
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
const today = formatToday();
// Perform first search for MOW
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Perform second search for LED
await page.goto(localePath(`onlineboard/departure/LED-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Perform third search for SVO
await page.goto(localePath(`onlineboard/departure/SVO-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historyItems = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
const count = await historyItems.count();
if (count < 2) {
test.skip(true, 'Insufficient history items for ordering test');
return;
}
// Get all items' text
const firstItemText = await historyItems.nth(0).textContent();
const secondItemText = await historyItems.nth(1).textContent();
// Most recent (SVO) should be first, older (LED) should be second
expect(firstItemText).toMatch(/SVO|сво/i);
expect(secondItemText).toMatch(/LED|лед/i);
});
test('323: Search history item shows search context or timestamp', async ({
page,
app,
locale,
localePath,
}) => {
// Perform a search
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
// Item should be visible with content
await expect(historyItem).toBeVisible();
// Check for some content (search info or time)
const text = await historyItem.textContent();
expect(text?.trim().length).toBeGreaterThan(0);
// Optional: check for time or date indicator
const hasTimeOrDate = /\d{1,2}:\d{2}|Today|Сегодня|今日/i.test(text || '');
// Not required, but nice to have
test.info().annotations.push({
type: 'warning',
description: hasTimeOrDate ? 'Timestamp present' : 'No timestamp visible in item',
});
});
// Test 324-328: Search History Interaction
test('324: Clicking search history item re-executes the search', async ({
page,
app,
locale,
localePath,
}) => {
// Perform initial search
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Click history item
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
const link = historyItem.locator('a').first();
const linkCount = await link.count();
if (linkCount === 0) {
test.skip(true, 'History item not a link');
return;
}
// Click and verify navigation
await link.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Should be on a search results page
const url = page.url();
expect(url).toMatch(/onlineboard/);
expect(url).toMatch(/departure|arrival|route/);
});
test('325: Re-executed search navigates to results page with correct parameters', async ({
page,
app,
locale,
localePath,
}) => {
const today = formatToday();
const searchUrl = localePath(`onlineboard/departure/MOW-${today}`);
// Navigate to search with known parameters
await page.goto(searchUrl);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Click history item
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
const link = historyItem.locator('a').first();
const linkCount = await link.count();
if (linkCount === 0) {
test.skip(true, 'History item not a link');
return;
}
// Click to re-execute
await link.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Verify URL contains departure and date
const url = page.url();
expect(url).toMatch(/departure/);
expect(url).toMatch(/MOW/);
expect(url).toMatch(today);
});
test('326: Re-executed search results page contains flight results', async ({
page,
app,
locale,
localePath,
}) => {
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Click history item
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
const link = historyItem.locator('a').first();
const linkCount = await link.count();
if (linkCount === 0) {
test.skip(true, 'History item not a link');
return;
}
await link.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
// Verify results are displayed
const results = page.locator(
`${tid(S.BOARD_SEARCH_RESULT, app)}, ${tid(S.BOARD_FLIGHT_RESULT, app)}`,
);
const resultCount = await results.count();
// May have 0 results if no flights, which is OK
// Just verify page didn't error
expect(resultCount).toBeGreaterThanOrEqual(0);
// Verify we have some content (board, day tabs, etc.)
const dayTabs = page.locator(`${tid(S.BOARD_DAY_TABS, app)}, ${tid(S.BOARD_DAY_TAB, app)}`);
const tabCount = await dayTabs.count();
if (tabCount === 0) {
// May not have day tabs if empty, but page should be on results page
const url = page.url();
expect(url).toMatch(/departure/);
}
});
test('327: History does not have visible delete button (or delete is not the focus)', async ({
page,
app,
locale,
localePath,
}) => {
// Note: React SearchHistory component does not implement delete functionality
// This test verifies we don't accidentally add complex delete features
const today = formatToday();
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const historyItem = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app)).first();
const count = await historyItem.count();
if (count === 0) {
test.skip(true, 'Search history items not available');
return;
}
// Look for delete button - there should not be one in current implementation
const deleteButton = historyItem.locator(
'[class*="delete"], [class*="remove"], button:has-text(/delete|remove|×)/i',
);
const deleteCount = await deleteButton.count();
// Current implementation doesn't have delete buttons on items
expect(deleteCount).toBe(0);
});
test('328: History items remain clickable for navigation throughout session', async ({
page,
app,
locale,
localePath,
}) => {
const today = formatToday();
// Perform two searches
await page.goto(localePath(`onlineboard/departure/MOW-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
await page.goto(localePath(`onlineboard/departure/LED-${today}`));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
// Go to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// First click to history item
const items = page.locator(tid(S.LANDING_SEARCH_HISTORY_ITEM, app));
const itemCount = await items.count();
if (itemCount === 0) {
test.skip(true, 'Search history items not available');
return;
}
// Click first item
const firstLink = items.nth(0).locator('a').first();
if ((await firstLink.count()) > 0) {
await firstLink.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(800);
}
// Return to landing
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Second click should still work
const secondLink = items.nth(0).locator('a').first();
if ((await secondLink.count()) > 0) {
await secondLink.click();
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
const url = page.url();
expect(url).toMatch(/onlineboard/);
}
});
// Test 329-331: Search History Persistence with Cross-App Isolation
test('329: should persist search history within same app instance', async ({
browser,
page,
localePath,
}) => {
const context = await browser.newContext();
const appPage = await context.newPage();
// Clear history first to ensure clean state
await appPage.goto(localePath('onlineboard'));
await appPage.waitForLoadState('networkidle');
await appPage.evaluate(() => {
localStorage.setItem('aeroflot_search_history', JSON.stringify([]));
});
// Create a mock search history entry (simulating what happens when user searches)
const mockEntry = {
id: 'test1',
label: 'Moscow Search',
url: '/ru-ru/onlineboard/departure/MOW-20260409',
timestamp: Date.now(),
};
// Inject the entry directly to simulate search
await appPage.evaluate((entry) => {
const history = [entry];
localStorage.setItem('aeroflot_search_history', JSON.stringify(history));
}, mockEntry);
// Navigate away and back to verify persistence
await appPage.goto(localePath('onlineboard'));
await appPage.waitForLoadState('networkidle');
await appPage.waitForTimeout(500);
const historyValue = await appPage.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(historyValue.length).toBeGreaterThan(0);
// History item should have required fields
expect(historyValue[0]).toHaveProperty('label');
expect(historyValue[0]).toHaveProperty('url');
expect(historyValue[0]).toHaveProperty('timestamp');
expect(historyValue[0].label).toContain('Moscow');
await context.close();
});
test('330: should isolate history between different app instances', async ({
browser,
page,
localePath,
}) => {
const context1 = await browser.newContext();
const page1 = await context1.newPage();
// Initialize context 1 with MOW search
await page1.goto(localePath('onlineboard'));
await page1.waitForLoadState('networkidle');
const mockEntry1 = {
id: 'mow1',
label: 'Moscow Search',
url: '/ru-ru/onlineboard/departure/MOW-20260409',
timestamp: Date.now(),
};
await page1.evaluate((entry) => {
localStorage.setItem('aeroflot_search_history', JSON.stringify([entry]));
}, mockEntry1);
// Verify context 1 has MOW
const history1 = await page1.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(history1.length).toBeGreaterThan(0);
expect(history1[0].label).toContain('Moscow');
// Create a second isolated context (different browser context = different localStorage)
const context2 = await browser.newContext();
const page2 = await context2.newPage();
await page2.goto(localePath('onlineboard'));
await page2.waitForLoadState('networkidle');
// Second instance should have empty history (isolated localStorage)
const history2Initial = await page2.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(history2Initial.length).toBe(0);
// Add LED search to context 2
const mockEntry2 = {
id: 'led2',
label: 'Saint Petersburg Search',
url: '/ru-ru/onlineboard/departure/LED-20260409',
timestamp: Date.now(),
};
await page2.evaluate((entry) => {
localStorage.setItem('aeroflot_search_history', JSON.stringify([entry]));
}, mockEntry2);
// Verify context 2 has LED
const history2After = await page2.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(history2After.length).toBeGreaterThan(0);
expect(history2After[0].label).toContain('Saint Petersburg');
// Verify context 1 still has MOW and NOT LED (isolated storage)
const history1Final = await page1.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(history1Final.length).toBe(1);
expect(history1Final[0].label).toContain('Moscow');
expect(JSON.stringify(history1Final[0]).includes('LED')).toBe(false);
await context1.close();
await context2.close();
});
test('331: should preserve recent search history entries', async ({ page, localePath }) => {
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
// Create multiple recent entries
const now = Date.now();
const entries = [
{
id: '1',
label: 'Moscow Search',
url: '/ru-ru/onlineboard/departure/MOW-20260409',
timestamp: now,
},
{
id: '2',
label: 'Saint Petersburg Search',
url: '/ru-ru/onlineboard/departure/LED-20260409',
timestamp: now - 1000,
},
{
id: '3',
label: 'Yekaterinburg Search',
url: '/ru-ru/onlineboard/departure/SVX-20260409',
timestamp: now - 2000,
},
];
// Store entries
await page.evaluate((entries) => {
localStorage.setItem('aeroflot_search_history', JSON.stringify(entries));
}, entries);
// Reload page
await page.goto(localePath('onlineboard'));
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Verify all entries are preserved
const history = await page.evaluate(() => {
const stored = localStorage.getItem('aeroflot_search_history');
return stored ? JSON.parse(stored) : [];
});
expect(history.length).toBe(3);
expect(history[0].label).toContain('Moscow');
expect(history[1].label).toContain('Saint Petersburg');
expect(history[2].label).toContain('Yekaterinburg');
// Verify they're stored with correct timestamp order (most recent first)
for (let i = 0; i < history.length - 1; i++) {
expect(history[i].timestamp).toBeGreaterThanOrEqual(history[i + 1].timestamp);
}
});
});
function formatToday(): string {
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
}