Strona /firmy, CTA jako linki i testy Playwright routingu
Nowa trasa:
- /firmy - lista firm i uslug na istniejacych komponentach (.directory-card)
i stylistyce sekcji uslug; filtr kategorii w query stringu (/firmy?kategoria=remonty)
- serviceCategories dostaly slug, nowa baza companyDirectory pokrywa wszystkie
8 kategorii, wiec filtrowanie dziala na realnych danych
- "Zobacz firmy" prowadzi do listy z wybrana kategoria, "Zobacz wszystkie"
do pelnej listy - ostatnie dwie atrapy href="#" usuniete
CTA zamienione na <Link> (wylacznie czysta nawigacja, bez akcji):
- 35 przyciskow onClick={() => navigate(x)} zamienionych automatycznie
- 5 skrotow panelu admina (setTab) -> Link do /admin/*
- karty narzedzi konta i kafelek poradnika: zamiast role="button" prawdziwy link
- pozycje menu konta i popovera powiadomien -> Link + zamkniecie popovera
- karty ofert: link rozciagniety na cala karte (.card-stretched-link) zamiast
role="button", bez zagniezdzania przyciskow w <a>
- bez zmian zostaly przyciski wykonujace akcje: publikacja, zapis draftu,
wyszukiwanie, oznaczanie powiadomien, bramki logowania
Wyglad 1:1 - rozszerzone selektory kontenerow o warianty <a>, reset :where(a)
o zerowej specyficznosci oraz odtworzenie centrowania tam, gdzie <button>
robil to domyslnie. Zweryfikowane zrzutami ekranu i pomiarem geometrii.
Naprawa: kazdy blad /auth/me (takze chwilowy timeout albo 5xx) kasowal token
i wylogowywal uzytkownika. Teraz sesje konczy tylko odrzucenie tokenu (401/403),
pozostale bledy sa ponawiane raz.
Testy: frontend/tests/routing.spec.ts (41 testow) + playwright.config.ts,
skrypt npm run test:e2e. E2E_BASE_URL pozwala uruchomic je na buildzie
produkcyjnym za nginx.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
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 <Link>/<NavLink>. 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 <a href> - 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user