import { test, expect, type Page } from '@playwright/test'; import { ROUTES } from '../src/routes'; // Standard projektu (AGENTS.md): kazdy widok ma wlasny URL obslugiwany przez React Router, // a nawigacja odbywa sie przez /. Te testy pilnuja tej zasady. const ADMIN = { email: 'admin@mieszko.pl', password: 'Admin123!' }; const PUBLIC_ROUTES = [ ROUTES.home, ROUTES.buy, ROUTES.rent, ROUTES.sell, ROUTES.valuation, ROUTES.priceHistory, ROUTES.districtRanking, ROUTES.negotiation, ROUTES.creditCalculator, ROUTES.map, ROUTES.services, ROUTES.companies, ROUTES.guides, ROUTES.guideBuying, ROUTES.login, ]; const PROTECTED_ROUTES = [ ROUTES.account, ROUTES.favorites, ROUTES.messages, ROUTES.add, ROUTES.admin, `${ROUTES.admin}/leady`, ]; async function goto(page: Page, path: string) { await page.goto(path, { waitUntil: 'domcontentloaded' }); await page.locator('main').first().waitFor({ state: 'attached' }); // ProtectedRoute dociaga profil z /auth/me - poczekaj, az skonczy stan ladowania await page.locator('.route-loading').waitFor({ state: 'detached' }).catch(() => {}); await page.waitForTimeout(400); } async function login(page: Page) { await goto(page, ROUTES.login); await page.getByPlaceholder('Wpisz swój adres e-mail').fill(ADMIN.email); await page.getByPlaceholder('Wpisz swoje hasło').fill(ADMIN.password); await page.getByRole('button', { name: 'Zaloguj się', exact: true }).click(); // token w localStorage to pewny dowod zalogowania - adres moze sie jeszcze zmieniac await page.waitForFunction(() => Boolean(window.localStorage.getItem('polskalokalnie-auth-token'))); await page.waitForFunction(() => window.location.pathname !== '/logowanie'); } test.describe('Trasy publiczne', () => { for (const path of PUBLIC_ROUTES) { test(`bezposrednie wejscie na ${path} (F5 / nowa karta / wklejony link)`, async ({ page }) => { await goto(page, path); expect(new URL(page.url()).pathname).toBe(path); await expect(page.locator('main')).not.toBeEmpty(); }); } test('nieznany adres pokazuje strone 404', async ({ page }) => { await goto(page, '/adres-ktory-nie-istnieje'); await expect(page.locator('.not-found-page')).toBeVisible(); }); test('odswiezenie strony zachowuje widok', async ({ page }) => { await goto(page, ROUTES.districtRanking); await page.reload({ waitUntil: 'domcontentloaded' }); expect(new URL(page.url()).pathname).toBe(ROUTES.districtRanking); }); }); test.describe('Lista firm i uslug (/firmy)', () => { test('wejscie na /firmy pokazuje pelna liste', async ({ page }) => { await goto(page, ROUTES.companies); await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi'); const cards = await page.locator('.directory-card').count(); expect(cards).toBeGreaterThan(10); }); test('filtr kategorii dziala przez query string', async ({ page }) => { await goto(page, `${ROUTES.companies}?kategoria=remonty`); await expect(page.locator('.companies-head h1')).toHaveText('Remont i wykończenie'); const filtered = await page.locator('.directory-card').count(); await goto(page, ROUTES.companies); const all = await page.locator('.directory-card').count(); expect(filtered).toBeGreaterThan(0); expect(filtered).toBeLessThan(all); }); test('nieznana kategoria nie wywraca widoku - pokazuje pelna liste', async ({ page }) => { await goto(page, `${ROUTES.companies}?kategoria=nie-ma-takiej`); await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi'); }); test('klik w chip kategorii zmienia adres i wynik', async ({ page }) => { await goto(page, ROUTES.companies); await page.locator('.companies-chip', { hasText: 'Finanse' }).first().click(); await page.waitForFunction(() => window.location.search.includes('kategoria=finanse')); await expect(page.locator('.companies-head h1')).toHaveText('Finanse'); }); test('"Zobacz firmy" prowadzi do listy z wybrana kategoria', async ({ page }) => { await goto(page, ROUTES.services); const link = page.locator('.service-category-card', { hasText: 'Remont i wykończenie' }).getByRole('link'); await expect(link).toHaveAttribute('href', '/firmy?kategoria=remonty'); await link.click(); await page.waitForFunction(() => window.location.pathname === '/firmy'); await expect(page.locator('.companies-head h1')).toHaveText('Remont i wykończenie'); }); test('"Zobacz wszystkie" prowadzi do pelnej listy', async ({ page }) => { await goto(page, ROUTES.services); const link = page.locator('.services-section .section-head', { hasText: 'Polecane firmy' }).getByRole('link'); await expect(link).toHaveAttribute('href', ROUTES.companies); await link.click(); await page.waitForFunction(() => window.location.pathname === '/firmy'); await expect(page.locator('.companies-head h1')).toHaveText('Wszystkie firmy i usługi'); }); test('Wstecz i Dalej dzialaja na filtrze kategorii', async ({ page }) => { await goto(page, ROUTES.services); await page.locator('.service-category-card', { hasText: 'Finanse' }).getByRole('link').click(); await page.waitForFunction(() => window.location.search.includes('kategoria=finanse')); await page.goBack({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => window.location.pathname === '/firmy-i-uslugi'); await page.goForward({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => window.location.search.includes('kategoria=finanse')); }); }); test.describe('Nawigacja przez linki', () => { test('menu glowne to - mozna otworzyc w nowej karcie', async ({ page }) => { await goto(page, ROUTES.home); for (const [label, expected] of [ ['Kupuję', ROUTES.buy], ['Wynajmuję', ROUTES.rent], ['Poradniki', ROUTES.guides], ['Firmy i usługi', ROUTES.services], ] as const) { const link = page.getByRole('link', { name: label, exact: true }).first(); await expect(link).toHaveAttribute('href', expected); } }); test('aktywna pozycja menu wynika z adresu', async ({ page }) => { await goto(page, ROUTES.buy); await expect(page.getByRole('link', { name: 'Kupuję', exact: true }).first()).toHaveClass(/active/); }); test('CTA naglowka maja poprawne adresy', async ({ page }) => { await goto(page, ROUTES.home); await expect(page.locator('a.post-button')).toHaveAttribute('href', ROUTES.add); await expect(page.getByRole('link', { name: 'Moje konto' })).toHaveAttribute('href', ROUTES.login); }); test('CTA w tresci strony sa linkami z href', async ({ page }) => { await goto(page, ROUTES.sell); await expect(page.locator('a.sell-primary')).toHaveAttribute('href', ROUTES.add); await expect(page.locator('.sell-valuation-card a')).toHaveAttribute('href', ROUTES.valuation); }); test('karta oferty ma prawdziwy link do strony oferty', async ({ page }) => { await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const card = page.locator('.real-result-card').first(); const href = await card.locator('.card-stretched-link').getAttribute('href'); expect(href).toMatch(/^\/oferta\/\d+$/); await expect(card.locator('a.real-card-cta')).toHaveAttribute('href', href!); }); test('klik w serce na karcie nie nawiguje', async ({ page }) => { await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); await page.locator('.real-result-card').first().locator('.real-favorite-button').click(); await page.waitForTimeout(700); expect(new URL(page.url()).pathname).toBe(ROUTES.buy); }); test('klik w menu zmienia adres, Wstecz i Dalej dzialaja', async ({ page }) => { await goto(page, ROUTES.home); await page.getByRole('link', { name: 'Kupuję', exact: true }).first().click(); await page.waitForFunction(() => window.location.pathname === '/kupno'); await page.getByRole('link', { name: 'Poradniki', exact: true }).first().click(); await page.waitForFunction(() => window.location.pathname === '/poradniki'); await page.goBack({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => window.location.pathname === '/kupno'); await page.goForward({ waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => window.location.pathname === '/poradniki'); }); }); test.describe('Standard: brak atrap linkow', () => { const PAGES = [ROUTES.home, ROUTES.services, ROUTES.companies, ROUTES.guides, ROUTES.sell, ROUTES.buy]; for (const path of PAGES) { test(`${path} nie zawiera href="#" ani javascript:void`, async ({ page }) => { await goto(page, path); const bad = await page.locator('a[href="#"], a[href^="javascript:"]').count(); expect(bad).toBe(0); }); } }); test.describe('Trasy chronione', () => { test('bez logowania przekierowuja na /logowanie', async ({ page }) => { for (const path of PROTECTED_ROUTES) { await goto(page, path); expect(new URL(page.url()).pathname).toBe(ROUTES.login); } }); test('po zalogowaniu wracamy na zamierzona strone', async ({ page }) => { await goto(page, ROUTES.accountPriceAlerts); expect(new URL(page.url()).pathname).toBe(ROUTES.login); await page.getByPlaceholder('Wpisz swój adres e-mail').fill(ADMIN.email); await page.getByPlaceholder('Wpisz swoje hasło').fill(ADMIN.password); await page.getByRole('button', { name: 'Zaloguj się', exact: true }).click(); await page.waitForFunction(() => window.location.pathname === '/konto/alerty-cenowe'); }); test('zakladki panelu admina maja wlasne adresy i przezywaja F5', async ({ page }) => { await login(page); await goto(page, ROUTES.admin); await page.locator('.admin-nav a', { hasText: 'Leady' }).first().click(); await page.waitForFunction(() => window.location.pathname === '/admin/leady'); await page.reload({ waitUntil: 'domcontentloaded' }); await page.locator('.admin-nav').first().waitFor({ state: 'attached' }); expect(new URL(page.url()).pathname).toBe('/admin/leady'); }); test('nieznana sekcja panelu admina pokazuje 404', async ({ page }) => { await login(page); await goto(page, `${ROUTES.admin}/nie-ma-takiej-sekcji`); await expect(page.locator('.not-found-page')).toBeVisible(); }); }); test.describe('Layout konta', () => { // Kazda pozycja lewego menu ma otwierac sie tak samo jak /konto/wyszukiwania, // czyli razem z widocznym menu konta. const ACCOUNT_PAGES = [ ROUTES.account, ROUTES.accountSearches, ROUTES.favorites, ROUTES.accountPriceAlerts, ROUTES.comparison, ROUTES.messages, ROUTES.accountMeetings, ROUTES.accountListings, ROUTES.accountSettings, ROUTES.notifications, ROUTES.accountHelpContact, ]; // Narzedzia publiczne dzialaja jak /kupno czy /wynajem - bez menu konta. const PUBLIC_TOOLS = [ROUTES.valuation, ROUTES.priceHistory, ROUTES.districtRanking, ROUTES.negotiation]; test('lewe menu konta jest widoczne na wszystkich stronach konta', async ({ page }) => { await login(page); for (const path of ACCOUNT_PAGES) { await goto(page, path); await expect(page.locator('.account-sidebar'), `brak lewego menu na ${path}`).toBeVisible(); } }); test('lewe menu nie przeskakuje miedzy stronami konta', async ({ page }) => { await login(page); const positions: Array<{ path: string; x: number; width: number }> = []; for (const path of ACCOUNT_PAGES) { await goto(page, path); const box = await page.locator('.account-sidebar').first().boundingBox(); positions.push({ path, x: Math.round(box!.x), width: Math.round(box!.width) }); } const first = positions[0]; for (const pos of positions) { expect(pos.x, `menu przesuniete na ${pos.path}`).toBe(first.x); expect(pos.width, `inna szerokosc menu na ${pos.path}`).toBe(first.width); } }); test('narzedzia publiczne nie pokazuja menu konta - takze po zalogowaniu', async ({ page }) => { for (const path of PUBLIC_TOOLS) { await goto(page, path); await expect(page.locator('.account-sidebar'), `menu konta na publicznej ${path}`).toHaveCount(0); } await login(page); for (const path of PUBLIC_TOOLS) { await goto(page, path); await expect(page.locator('.account-sidebar'), `menu konta na publicznej ${path}`).toHaveCount(0); } }); // Bezpieczenstwo i edycja profilu byly osobnymi stronami powtarzajacymi tresc ustawien. // Teraz sa sekcjami /konto/ustawienia, a stare adresy maja przekierowywac. test('stare adresy profilu i bezpieczenstwa przekierowuja do ustawien', async ({ page }) => { await login(page); for (const path of [ROUTES.accountSecurity, ROUTES.accountProfileEdit]) { await goto(page, path); expect(new URL(page.url()).pathname, `${path} nie przekierowal`).toBe(ROUTES.accountSettings); } }); test('ustawienia pokazuja dane podane przy rejestracji i nie dubluja sekcji', async ({ page }) => { await login(page); await goto(page, ROUTES.accountSettings); // E-mail konta jest wypelniony automatycznie i nie da sie go tu zmienic. const emailInput = page.locator('.settings-card input[disabled]').first(); await expect(emailInput).toHaveValue(ADMIN.email); // Nick nadany przy zakladaniu konta jest widoczny w formularzu. await expect(page.getByText('polskalokalnie.pl/u/', { exact: false }).first()).toBeVisible(); // Jezyk komunikacji wystepowal wczesniej dwa razy na jednym ekranie. await expect(page.getByText('Język komunikacji', { exact: true })).toHaveCount(1); // Bezpieczenstwo jest dokladnie jedna sekcja, nie karta + osobna strona. await expect(page.locator('section[aria-label="Bezpieczeństwo"]')).toHaveCount(1); }); test('menu uzytkownika w naglowku ma pozycje ulozone w jednej linii', async ({ page }) => { await login(page); await goto(page, ROUTES.home); await page.locator('.account-menu-trigger').click(); const items = page.locator('.account-popover a'); await expect(items.first()).toBeVisible(); for (let i = 0; i < await items.count(); i += 1) { const box = await items.nth(i).boundingBox(); // ikona obok tekstu daje ~42px; zawiniecie do dwoch linii daje wyraznie wiecej expect(Math.round(box!.height), 'pozycja menu zawinela sie do dwoch linii').toBeLessThan(56); } }); }); test.describe('Naglowek', () => { test('zostaje przyklejony do gory przy przewijaniu', async ({ page }) => { for (const path of [ROUTES.home, ROUTES.buy, ROUTES.guides]) { await goto(page, path); await page.evaluate(() => window.scrollTo(0, 1400)); await page.waitForTimeout(300); const box = await page.locator('.header').boundingBox(); expect(Math.round(box!.y), `naglowek odjechal na ${path}`).toBe(0); } }); test('logo prowadzi na strone glowna i pokazuje nowa nazwe', async ({ page }) => { await goto(page, ROUTES.buy); const brand = page.locator('a.brand'); await expect(brand).toHaveAttribute('href', ROUTES.home); await expect(brand).toContainText('Polska Lokalnie'); await expect(brand).toContainText('ogłoszenia za darmo'); }); });