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.
766 lines
24 KiB
TypeScript
766 lines
24 KiB
TypeScript
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')}`;
|
||
}
|