import { test, expect, type Page } from '@playwright/test'; import { ROUTES, listingPath, negotiationPath, ADMIN_CONVERSATION_SLUG, conversationPath, newConversationPath, paymentPath, priceHistoryPath } 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@softspm.com', 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, ROUTES.loginForgot, ROUTES.register, ROUTES.registerPrivate, ROUTES.registerCompany, ]; const PROTECTED_ROUTES = [ ROUTES.account, ROUTES.favorites, ROUTES.messages, conversationPath(ADMIN_CONVERSATION_SLUG), newConversationPath(1), paymentPath(1), 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)', () => { // Do serwisu nie dolaczyla jeszcze zadna firma. Dopoki tak jest, /firmy pokazuje SAMO zaproszenie - // bez naglowka, licznika, chipow kategorii i wyszukiwarki (pokazywalyby zera i filtrowaly pustke). // Gdy pojawi sie pierwsza firma, gorna czesc wraca i te testy trzeba rozszerzyc o karty. test('wejscie na /firmy pokazuje samo zaproszenie', async ({ page }) => { await goto(page, ROUTES.companies); await expect(page.locator('.companies-none')).toBeVisible(); await expect(page.locator('.directory-card')).toHaveCount(0); await expect(page.locator('.companies-head')).toHaveCount(0); await expect(page.locator('.companies-filters')).toHaveCount(0); await expect(page.locator('.service-mini-search')).toHaveCount(0); }); test('zaproszenie przejmuje role naglowka H1', async ({ page }) => { await goto(page, ROUTES.companies); const headings = page.locator('.companies-page h1'); await expect(headings).toHaveCount(1); await expect(headings).toContainText('Nie mamy jeszcze żadnych firm'); }); test('kategoria z query stringu trafia do komunikatu', async ({ page }) => { await goto(page, `${ROUTES.companies}?kategoria=remonty`); await expect(page.locator('.companies-none')).toContainText('Remont i wykończenie'); await goto(page, ROUTES.companies); await expect(page.locator('.companies-none')).toContainText('w naszej bazie'); }); test('nieznana kategoria nie wywraca widoku', async ({ page }) => { await goto(page, `${ROUTES.companies}?kategoria=nie-ma-takiej`); await expect(page.locator('.companies-none')).toBeVisible(); await expect(page.locator('.companies-none')).toContainText('w naszej bazie'); }); // Kategorii usług jeszcze nie obsługujemy, więc kafelek prowadzi na stronę zgłoszenia // reklamowego (/reklama), a nie do listy firm - z kategorią przeniesioną w query stringu. test('"Zobacz firmy" prowadzi na strone reklamy 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', '/reklama?kategoria=remonty'); await link.click(); await page.waitForFunction(() => window.location.pathname === '/reklama'); await expect(page.locator('.advertising-input.select select')).toHaveValue('Remont i wykończenie'); }); test('"Zobacz wszystkie" prowadzi na /firmy', 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-none')).toBeVisible(); }); 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); } }); // Narzedzia sa publiczne: gosc widzi sama strone, a zalogowany takze lewe menu konta - // dzieki temu menu nie znika po otwarciu narzedzia z panelu. test('narzedzia publiczne: bez menu dla goscia, z menu po zalogowaniu', async ({ page }) => { for (const path of PUBLIC_TOOLS) { await goto(page, path); await expect(page.locator('.account-sidebar'), `menu konta u goscia na ${path}`).toHaveCount(0); } await login(page); for (const path of PUBLIC_TOOLS) { await goto(page, path); await expect(page.locator('.account-sidebar'), `brak menu konta na ${path}`).toBeVisible(); } }); // 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'); }); }); test.describe('Wiadomosci', () => { test('"Napisz wiadomosc" w ogloszeniu otwiera rozmowe pod wlasnym adresem', async ({ page }) => { await login(page); await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); await goto(page, href!); await page.getByRole('button', { name: /Napisz wiadomość/ }).first().click(); await page.waitForFunction(() => /^\/wiadomosci\/nowa\/\d+$/.test(window.location.pathname)); // Adres szkicu dziala po odswiezeniu - rozmowa nie zalezy od stanu nawigacji. await page.reload(); await expect(page.locator('.messages-composer input[type=text]')).toBeVisible(); }); test('nieistniejaca rozmowa wraca na liste wiadomosci', async ({ page }) => { await login(page); await goto(page, conversationPath(999999)); await page.waitForFunction(() => window.location.pathname === '/wiadomosci'); }); }); test.describe('Logowanie i rejestracja', () => { test('kroki maja wlasne adresy, a Wstecz wraca do poprzedniego kroku', async ({ page }) => { await goto(page, ROUTES.login); await page.getByRole('button', { name: 'Zarejestruj się' }).first().click(); await page.waitForFunction(() => window.location.pathname === '/rejestracja'); await page.locator('.register-choice-option').first().click(); await page.waitForFunction(() => window.location.pathname === '/rejestracja/osoba-prywatna'); await page.goBack(); await page.waitForFunction(() => window.location.pathname === '/rejestracja'); }); test('ekran "sprawdz poczte" bez rejestracji wraca do rejestracji', async ({ page }) => { await goto(page, ROUTES.registerPending); await page.waitForFunction(() => window.location.pathname === '/rejestracja'); }); }); test.describe('Wycena', () => { test('kroki maja wlasne adresy, a dane przetrwaja odswiezenie', async ({ page }) => { await goto(page, ROUTES.valuation); await page.locator('.valuation-form-section input').first().fill('Warszawa'); await page.getByRole('button', { name: /Dalej/ }).click(); await page.waitForFunction(() => window.location.pathname === '/wycena/szczegoly'); await page.reload(); await page.goBack(); await page.waitForFunction(() => window.location.pathname === '/wycena'); await expect(page.locator('.valuation-form-section input').first()).toHaveValue('Warszawa'); }); test('pusty wynik prowadzi do kroku z brakujacymi danymi', async ({ page }) => { await goto(page, ROUTES.valuationResult); await expect(page.locator('.valuation-result-hero strong').first()).toHaveText('Brakuje danych do wyceny'); await page.locator('.valuation-missing-item').first().click(); await page.waitForFunction(() => window.location.pathname === '/wycena'); }); }); test.describe('Negocjacje', () => { test('"Ile moge utargowac?" w ogloszeniu sprzedazy otwiera analize pod adresem z numerem', async ({ page }) => { await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); await goto(page, href!); await page.getByRole('link', { name: /Ile mogę utargować/ }).click(); await page.waitForFunction(() => /^\/negocjacje\/\d+$/.test(window.location.pathname)); await page.reload(); await expect(page.locator('.negotiation-offer h2')).toBeVisible(); await expect(page.locator('.negotiation-range')).not.toContainText('Pobieramy', { timeout: 15000 }); }); test('nieistniejace ogloszenie pokazuje komunikat', async ({ page }) => { await goto(page, negotiationPath(999999)); await expect(page.getByText('Nie znaleźliśmy tego ogłoszenia')).toBeVisible(); }); }); test.describe('Standard: przyciski robia to, co obiecuja', () => { test('wyszukiwarka poradnikow trzyma fraze w adresie i przezywa F5', async ({ page }) => { await goto(page, ROUTES.guides); await page.locator('.guides-search input').fill('kredyt'); await page.locator('.guides-search button').click(); await page.waitForFunction(() => window.location.search.includes('szukaj=kredyt')); await page.reload(); await expect(page.locator('section[aria-label="Wyniki wyszukiwania"]')).toContainText('kredyt'); }); test('"Szukaj na biezacej mapie" zaweza liste do ofert z mapy', async ({ page }) => { await goto(page, ROUTES.buy); await page.locator('.map-card').first().waitFor(); await page.getByRole('button', { name: /Szukaj na bieżącej mapie/ }).first().click(); await page.waitForFunction(() => window.location.search.includes('obszar=')); await expect(page.locator('.map-area-note')).toBeVisible(); await page.locator('.map-area-note button').click(); await page.waitForFunction(() => !window.location.search.includes('obszar=')); }); test('porownanie trzyma wybrane oferty w adresie', async ({ page }) => { await login(page); await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); const listingId = Number(href!.split('/').pop()); await goto(page, `${ROUTES.comparison}?oferty=${listingId}`); await expect(page.locator('.comparison-main')).toContainText('Zobacz ogłoszenie'); await page.reload(); await expect(page.locator('.comparison-main')).toContainText('Zobacz ogłoszenie'); }); test('kanaly kontaktu w pomocy to prawdziwe linki mailto i tel', async ({ page }) => { await login(page); await goto(page, ROUTES.accountHelpContact); await expect(page.locator('a.help-contact-channel-action[href^="mailto:"]')).toHaveCount(1); await expect(page.locator('a.help-contact-channel-action[href^="tel:"]')).toHaveCount(1); }); }); // Zapisane wyszukiwania trzyma serwer, wiec sa te same w kazdej przegladarce i przezywaja // wyczyszczenie danych strony. Bez powiadomien - to tylko lista filtrow do wygodnego powrotu. test.describe('Zapisane wyszukiwania', () => { test('zapis przezywa wyczyszczenie pamieci przegladarki', async ({ page }) => { await login(page); await goto(page, `${ROUTES.buy}?miasto=Gdynia`); const saveButton = page.getByRole('button', { name: /wyszukiwanie/ }).first(); if ((await saveButton.innerText()).includes('Zapisano')) { await saveButton.click(); await expect(saveButton).toHaveText(/Zapisz wyszukiwanie/); } await saveButton.click(); await expect(saveButton).toHaveText(/Zapisano wyszukiwanie/); // Symulacja innego urzadzenia: zostaje sam token logowania, reszta pamieci znika. await page.evaluate(() => { const token = window.localStorage.getItem('polskalokalnie-auth-token'); window.localStorage.clear(); if (token) { window.localStorage.setItem('polskalokalnie-auth-token', token); } }); await goto(page, ROUTES.accountSearches); await expect(page.locator('.account-searches-list, main').first()).toContainText('Gdynia'); // Sprzatanie, zeby kolejne uruchomienie testu zaczynalo od czystej listy. await page.locator('.account-search-remove').first().click(); await expect(page.locator('main')).not.toContainText('Gdynia'); }); }); // Spotkania sa na koncie, a ogladanie umawiaja obie strony rozmowy o ogloszeniu. test.describe('Spotkania', () => { test('wlasny wpis w kalendarzu przezywa odswiezenie', async ({ page }) => { await login(page); await goto(page, ROUTES.accountMeetings); await page.getByRole('button', { name: /Dodaj własny wpis/ }).click(); const title = `Notatka ${Date.now()}`; await page.locator('.meetings-create-form input[type=text]').first().fill(title); await page.getByRole('button', { name: 'Zapisz wpis' }).click(); await expect(page.locator('.meetings-list')).toContainText(title); await page.reload(); await expect(page.locator('.meetings-list')).toContainText(title); await page.locator('button[aria-label="Usuń wpis"]').first().click(); await expect(page.locator('.meetings-list')).not.toContainText(title); }); test('w rozmowie o ogloszeniu mozna zaproponowac ogladanie', async ({ page }) => { await login(page); await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); await goto(page, href!); await page.getByRole('button', { name: /Napisz wiadomość/ }).first().click(); await page.waitForFunction(() => window.location.pathname.startsWith('/wiadomosci/')); await page.locator('.messages-composer input[type=text]').fill('Dzien dobry, czy oferta jest aktualna?'); await page.locator('.messages-composer .send').click(); await page.waitForFunction(() => /wiadomosci/.test(window.location.pathname) && window.location.pathname.split('/').length === 3); await page.getByRole('button', { name: /Zaproponuj oglądanie/ }).click(); await expect(page.locator('.messages-meeting-form')).toBeVisible(); }); }); // Ulubione sa na koncie. Test pilnuje regresji: wejscie z pustej przegladarki NIE moze // skasowac ulubionych zapisanych wczesniej (kiedys front dyktowal serwerowi cala liste). test.describe('Ulubione', () => { test('przezywaja wyczyszczenie pamieci przegladarki', async ({ page }) => { await login(page); await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); await goto(page, href!); const title = await page.locator('.listing-detail-title, h1').first().innerText(); const heart = page.locator('button[aria-label*="ulubion"], .listing-detail-fav').first(); await heart.click(); await goto(page, ROUTES.favorites); await expect(page.locator('main')).toContainText(title.trim().slice(0, 20)); await page.evaluate(() => { const token = window.localStorage.getItem('polskalokalnie-auth-token'); window.localStorage.clear(); if (token) { window.localStorage.setItem('polskalokalnie-auth-token', token); } }); await goto(page, ROUTES.favorites); await expect(page.locator('main')).toContainText(title.trim().slice(0, 20)); await page.locator('button[aria-label*="Usuń"], .favorites-card-remove').first().click(); await expect(page.locator('main')).not.toContainText(title.trim().slice(0, 20)); }); }); // Lista sprawdzonych lokalizacji w Historii cen jest ustawieniem konta, nie pamiecia przegladarki. test.describe('Historia cen', () => { test('sprawdzone lokalizacje przezywaja wyczyszczenie pamieci przegladarki', async ({ page }) => { await login(page); await goto(page, ROUTES.priceHistory); await page.locator('.history-location-display').click(); await page.locator('.history-location-pop input').fill('Kraków'); await page.locator('.history-location-pop button', { hasText: 'Kraków' }).first().click(); await expect(page.locator('main')).toContainText('Kraków'); await page.evaluate(() => { const token = window.localStorage.getItem('polskalokalnie-auth-token'); window.localStorage.clear(); if (token) { window.localStorage.setItem('polskalokalnie-auth-token', token); } }); await goto(page, ROUTES.priceHistory); await expect(page.locator('main')).toContainText('Kraków'); }); }); // Alerty cenowe sa na koncie: zmiany wykrywa serwer, a powiadomienie trafia na liste w serwisie. test.describe('Alerty cenowe', () => { test('alert ogloszenia przezywa wyczyszczenie pamieci przegladarki', async ({ page }) => { await login(page); await goto(page, ROUTES.buy); await page.locator('.real-result-card').first().waitFor(); const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href'); await goto(page, href!); const alertButton = page.locator('.listing-detail-alert'); if ((await alertButton.innerText()).includes('aktywny')) { await alertButton.click(); await expect(alertButton).toHaveText(/Utwórz alert ceny/); } await alertButton.click(); await expect(alertButton).toHaveText(/Alert aktywny/); // Symulacja innego urzadzenia: zostaje sam token logowania. await page.evaluate(() => { const token = window.localStorage.getItem('polskalokalnie-auth-token'); window.localStorage.clear(); if (token) { window.localStorage.setItem('polskalokalnie-auth-token', token); } }); await goto(page, ROUTES.accountPriceAlerts); await expect(page.locator('.price-alerts-list-section')).not.toContainText('Nie obserwujesz jeszcze żadnego ogłoszenia'); // Sprzatanie: alert zdejmujemy tam, gdzie zostal zalozony. await goto(page, href!); await page.locator('.listing-detail-alert').click(); await expect(page.locator('.listing-detail-alert')).toHaveText(/Utwórz alert ceny/); }); }); // RODO: bez zgody nie ladujemy tresci z obcych serwisow, ktore dostalyby adres IP ogladajacego. test.describe('Prywatnosc', () => { test('wirtualny spacer laduje sie dopiero po zgodzie', async ({ page }) => { const externalHosts = new Set(); page.on('request', (request) => { const host = new URL(request.url()).hostname; if (!host.includes('localhost') && !host.includes('127.0.0.1')) { externalHosts.add(host); } }); // Szukamy ogloszenia ze spacerem - tylko tam jest co blokowac. const listings = await page.request.get('/api/listings?offerType=SALE').then((r) => r.json()); let tourListingId: number | null = null; for (const item of listings) { const detail = await page.request.get(`/api/listings/${item.id}`).then((r) => r.json()); if (detail.virtualTourUrl) { tourListingId = item.id; break; } } if (tourListingId == null) { test.skip(true, 'Zadne ogloszenie nie ma wirtualnego spaceru'); } await goto(page, listingPath(tourListingId!)); await expect(page.locator('.listing-detail-tour-consent')).toBeVisible(); await expect(page.locator('.listing-detail-tour-embed iframe')).toHaveCount(0); expect([...externalHosts].some((host) => host.includes('youtube') || host.includes('kuula') || host.includes('matterport'))).toBe(false); await page.getByRole('button', { name: /Pokaż wirtualny spacer/ }).click(); await expect(page.locator('.listing-detail-tour-embed iframe')).toHaveCount(1); }); }); // Telefon: poziome menu nie miesci sie w naglowku, wiec dzialy sa w szufladzie, // a najczestsze miejsca w dolnym pasku. Kazda pozycja to link ze zmiana adresu. test.describe('Nawigacja mobilna', () => { test.use({ viewport: { width: 390, height: 844 } }); test('naglowek nie przewija sie w bok, menu glowne chowa sie w szufladzie', async ({ page }) => { await goto(page, ROUTES.home); await expect(page.locator('.nav')).toBeHidden(); const overflow = await page.evaluate(() => { const de = document.documentElement; return de.scrollWidth - de.clientWidth; }); expect(overflow).toBeLessThanOrEqual(0); await page.getByRole('button', { name: 'Menu serwisu' }).click(); await expect(page.locator('.mobile-menu')).toBeVisible(); await page.locator('.mobile-menu').getByRole('link', { name: 'Wynajmuję' }).click(); await expect(page).toHaveURL(new RegExp(`${ROUTES.rent}$`)); await expect(page.locator('.mobile-menu')).toHaveCount(0); }); test('dolny pasek prowadzi pod wlasne adresy', async ({ page }) => { await goto(page, ROUTES.home); // Baner zgody stoi nad dolnym paskiem - na telefonie trzeba go najpierw zamknac. await page.getByRole('button', { name: 'Tylko niezbędne' }).click(); const bar = page.locator('.mobile-nav'); await expect(bar).toBeVisible(); await bar.getByRole('link', { name: 'Szukaj' }).click(); await expect(page).toHaveURL(new RegExp(`${ROUTES.buy}$`)); await bar.getByRole('link', { name: 'Dodaj ogłoszenie' }).click(); // Dodawanie wymaga logowania - goscia przejmuje ProtectedRoute. await expect(page).toHaveURL(new RegExp(`${ROUTES.login}$`)); await bar.getByRole('link', { name: 'Start' }).click(); await expect(page).toHaveURL(/\/$/); }); test('dolny pasek nie zaslania stopki', async ({ page }) => { await goto(page, ROUTES.buy); await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(400); const footerBottom = await page.locator('.site-footer').evaluate((el) => el.getBoundingClientRect().bottom); const barTop = await page.locator('.mobile-nav').evaluate((el) => el.getBoundingClientRect().top); expect(footerBottom).toBeLessThanOrEqual(barTop + 1); }); test('na desktopie nie ma dolnego paska ani przycisku menu', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 900 }); await goto(page, ROUTES.home); await expect(page.locator('.mobile-nav')).toBeHidden(); await expect(page.locator('.header-burger')).toBeHidden(); await expect(page.locator('.nav')).toBeVisible(); }); }); // Dzielnica bez wlasnych ogloszen nadal ma na wykresie ceny transakcyjne dla calego miasta - // naglowek i rubryki licza wtedy srednia z NBP i GUS, zamiast pokazywac same kreski. test.describe('Historia cen', () => { test('rubryki pokazuja liczby takze tam, gdzie nie ma jeszcze ogloszen', async ({ page }) => { await goto(page, priceHistoryPath('Gdańsk, Wrzeszcz')); await expect(page.locator('.history-chart-title-row strong')).not.toHaveText('—'); await expect(page.locator('.history-source-note')).toContainText(/NBP i GUS|Polska Lokalnie/); // Najnizsza, najwyzsza i srednia maja sens przy jednym punkcie; zmiana w okresie wymaga dwoch, // wiec jej nie sprawdzamy - ma wtedy wlasny podpis "Za krotka historia". const values = page.locator('.history-stat-grid article strong'); await expect(values).toHaveCount(4); for (const index of [0, 1, 3]) { await expect(values.nth(index)).not.toHaveText('—'); } }); test('bez wybranej lokalizacji nie zmyslamy zadnych liczb', async ({ page }) => { await goto(page, ROUTES.priceHistory); await expect(page.locator('.history-chart-title-row strong')).toHaveText('—'); await expect(page.locator('.history-chart-empty')).toContainText('Wybierz lokalizację'); }); });