Files
BIU_System_Rezerwacji/test_rms.mjs
Krzysztof Cieślik f436d87ca5 Inicjalna wersja systemu zarządzania rezerwacjami (RMS)
SPA zbudowane w React 19 + Vite 8 z pełnym zestawem funkcjonalności:
autentykacja z 2FA, kreator rezerwacji, panel admina, analityka,
GraphQL (Apollo Client + SchemaLink), React Query, Storybook,
testy jednostkowe (Vitest + RTL) i e2e (Playwright).
2026-06-21 06:08:47 +02:00

336 lines
18 KiB
JavaScript

import { chromium } from 'playwright';
const BASE = 'http://localhost:5173';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// After a successful password submit, the app shows a 2FA screen with the code
// displayed in the UI. This helper waits for it, reads the code, and submits.
async function complete2FA(page) {
const codeEl = page.locator('[class*="code-number"]').first();
try {
await codeEl.waitFor({ state: 'visible', timeout: 4000 });
} catch {
return; // 2FA screen did not appear — user likely already navigated
}
const code = await codeEl.innerText().catch(() => '');
if (!code) return;
await page.locator('input#code').fill(code.trim());
await page.locator('button[type="submit"]').click();
await sleep(500);
}
// Reset seed reservations so the test is repeatable across runs.
async function resetDb() {
const api = 'http://localhost:3001';
const patch = (id, body) =>
fetch(`${api}/reservations/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
// Ensure r1 and r2 are pending so admin-confirm test always finds a button.
await patch('r1', { status: 'pending' });
await patch('r2', { status: 'pending' });
}
async function run() {
await resetDb();
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext();
const page = await ctx.newPage();
// Capture browser console errors
const pageErrors = [];
page.on('pageerror', (err) => pageErrors.push(err.message));
page.on('console', (msg) => {
if (msg.type() === 'error') pageErrors.push(msg.text());
});
const results = [];
const ok = (l, n='') => results.push(`PASS ${l}${n ? ' — '+n : ''}`);
const fail = (l, n='') => results.push(`FAIL ${l}${n ? ' — '+n : ''}`);
const warn = (l, n='') => results.push(`WARN ${l}${n ? ' — '+n : ''}`);
// ── 1. Login page loads ────────────────────────────────────────────────────
await page.goto(BASE + '/login');
await page.waitForLoadState('networkidle');
const hasEmailInput = await page.locator('input[type="email"]').isVisible().catch(() => false);
hasEmailInput ? ok('Login page loads (email field visible)') : fail('Login page missing email field');
// ── 2. Wrong credentials show error ───────────────────────────────────────
await page.fill('input[type="email"]', 'wrong@example.com');
await page.fill('input[type="password"]', 'wrongpass');
await page.click('button[type="submit"]');
await sleep(1000);
const errorVisible = await page.locator('text=/invalid|incorrect|wrong/i').isVisible().catch(() => false);
errorVisible ? ok('Wrong credentials → error message') : fail('Wrong credentials — no error shown');
// ── 3. Admin login ────────────────────────────────────────────────────────
await page.fill('input[type="email"]', 'admin@reservations.dev');
await page.fill('input[type="password"]', 'Admin1234!');
await page.click('button[type="submit"]');
await complete2FA(page);
await page.waitForURL('**/admin', { timeout: 5000 }).catch(() => {});
page.url().includes('/admin')
? ok('Admin login → /admin')
: fail('Admin redirect failed', page.url());
// ── 4. Admin table rows ───────────────────────────────────────────────────
await page.waitForSelector('table tbody tr', { timeout: 5000 }).catch(() => {});
const rowCount = await page.locator('tbody tr').count().catch(() => 0);
rowCount > 0 ? ok('Admin table', `${rowCount} rows`) : fail('Admin table empty');
// ── 5. Confirm button on pending row ─────────────────────────────────────
const confirmBtn = page.locator('[aria-label^="Confirm reservation"]').first();
const hasConfirm = await confirmBtn.isVisible().catch(() => false);
hasConfirm ? ok('Confirm button visible') : fail('Confirm button missing');
if (hasConfirm) {
await confirmBtn.click();
await sleep(1200);
const confirmedBadge = await page.locator('text=Confirmed').count();
confirmedBadge >= 1
? ok('Confirm click updates row to Confirmed')
: warn('Confirmed badge not found after click');
}
// ── 6. Delete modal ───────────────────────────────────────────────────────
// Wait for table to re-render after previous action
await page.waitForSelector('table tbody tr', { timeout: 4000 }).catch(() => {});
await sleep(500);
const deleteBtn = page.locator('[aria-label^="Delete reservation"]').first();
await deleteBtn.click().catch(() => {});
await sleep(600);
const modalVisible = await page.locator('[role="dialog"]').isVisible().catch(() => false);
modalVisible ? ok('Delete confirmation modal opens') : fail('Delete modal did not open');
// Dismiss via Cancel button
const cancelModalBtn = page.locator('[class*="modal__btn--cancel"]').first();
await cancelModalBtn.click().catch(() => {
page.keyboard.press('Escape').catch(() => {});
});
await sleep(300);
// ── 7. Calendar tab ───────────────────────────────────────────────────────
const calTab = page.locator('button', { hasText: /Calendar/i });
const hasCalTab = await calTab.isVisible().catch(() => false);
hasCalTab ? ok('Calendar tab button visible') : fail('Calendar tab missing');
if (hasCalTab) {
await calTab.click();
await sleep(600);
const monthTitle = await page.locator('text=/January|February|March|April|May|June|July|August|September|October|November|December/').first().innerText().catch(() => '');
monthTitle ? ok('Calendar view renders month', monthTitle) : fail('Calendar month title missing');
const monCell = await page.locator('text=Mon').first().isVisible().catch(() => false);
monCell ? ok('Calendar weekday headers render') : fail('Calendar weekday headers missing');
// Click a day cell that has bookings
const busyCell = page.locator('[class*="cell--busy"]').first();
const hasBusy = await busyCell.isVisible().catch(() => false);
if (hasBusy) {
await busyCell.click();
await sleep(400);
const panelVisible = await page.locator('[class*="cal__panel"]').isVisible().catch(() => false);
panelVisible ? ok('Clicking busy day opens side panel') : fail('Calendar side panel did not open');
} else {
warn('No busy day cells visible in current month view');
}
// Navigate to next month
await page.locator('[aria-label="Next month"]').click().catch(() => {});
await sleep(400);
const newMonth = await page.locator('text=/January|February|March|April|May|June|July|August|September|October|November|December/').first().innerText().catch(() => '');
newMonth !== monthTitle
? ok('Calendar next-month navigation', newMonth)
: fail('Calendar did not advance to next month');
}
// ── 8. Admin logout → client login ───────────────────────────────────────
await page.locator('button:has-text("Table")').click().catch(() => {});
await sleep(200);
await page.click('button:has-text("Sign out")').catch(() => {});
await page.waitForURL('**/login', { timeout: 3000 }).catch(() => {});
page.url().includes('/login') ? ok('Admin logout → /login') : warn('Logout redirect', page.url());
await page.fill('input[type="email"]', 'anna.kowalski@example.com');
await page.fill('input[type="password"]', 'Client1234!');
await page.click('button[type="submit"]');
await complete2FA(page);
await page.waitForURL('**/dashboard', { timeout: 5000 }).catch(() => {});
page.url().includes('/dashboard') ? ok('Client login → /dashboard') : fail('Dashboard redirect', page.url());
// ── 9. AvailabilitySearch ─────────────────────────────────────────────────
await page.waitForLoadState('networkidle');
await sleep(500);
const availTitle = await page.locator('text=Check Availability').isVisible().catch(() => false);
availTitle ? ok('AvailabilitySearch section renders') : fail('AvailabilitySearch missing');
// ── 10. URL sync ──────────────────────────────────────────────────────────
await page.locator('input[type="date"]').first().fill('2026-07-15');
await page.locator('select').first().selectOption({ index: 1 });
await sleep(1200);
const urlAfterFilter = page.url();
urlAfterFilter.includes('date=2026-07-15') ? ok('Date syncs to URL') : fail('Date param missing', urlAfterFilter);
urlAfterFilter.includes('service=') ? ok('Service syncs to URL') : fail('Service param missing', urlAfterFilter);
// ── 11. Time slot grid ────────────────────────────────────────────────────
const slotGrid = await page.locator('button:has-text("09:00")').isVisible().catch(() => false);
slotGrid ? ok('Time slot grid renders (09:00 visible)') : fail('Time slot grid missing');
// ── 12. Slot click pre-fills wizard ──────────────────────────────────────
const freeSlot = page.locator('[class*="slot--free"]').first();
const hasFreeSlot = await freeSlot.isVisible().catch(() => false);
if (hasFreeSlot) {
const slotText = await freeSlot.innerText().catch(() => '?');
await freeSlot.click();
await sleep(600);
const step3Active = await page.locator('text=/Special Requirements/i').isVisible().catch(() => false);
step3Active ? ok(`Slot click (${slotText}) jumps wizard to step 3`) : fail('Slot click did not advance wizard');
} else {
warn('No free slots for selected date/service');
}
// ── 13. BookingWizard step navigation ────────────────────────────────────
await page.goto(BASE + '/dashboard');
await page.waitForLoadState('networkidle');
await page.waitForSelector('[class*="wizard__step-title"]', { timeout: 5000 }).catch(() => {});
const wizardTitle = await page.locator('[class*="wizard__step-title"]').first().innerText().catch(() => '');
wizardTitle.includes('Select a Service')
? ok('Wizard renders at step 1 after reload')
: fail('Wizard step 1 title missing', `got: "${wizardTitle}"`);
// Pick a service card
const serviceCard = page.locator('[class*="service-card"]').first();
const hasCards = await serviceCard.isVisible().catch(() => false);
hasCards ? ok('Service cards render') : fail('Service cards missing');
if (hasCards) await serviceCard.click();
// Next → step 2
await page.locator('button:has-text("Next")').first().click().catch(() => {});
await sleep(400);
const step2Title = await page.locator('[class*="wizard__step-title"]').first().innerText().catch(() => '');
step2Title.includes('Date') ? ok('Wizard step 2 (Date & Time)') : fail('Step 2 not reached', step2Title);
// Fill date + time for a date with no conflicts
await page.locator('input[type="date"]').last().fill('2026-09-01');
await page.locator('input[type="time"]').last().fill('14:30');
await page.locator('button:has-text("Next")').first().click().catch(() => {});
await sleep(400);
const step3Title = await page.locator('[class*="wizard__step-title"]').first().innerText().catch(() => '');
step3Title.includes('Requirements') ? ok('Wizard step 3 (Requirements)') : fail('Step 3 not reached', step3Title);
await page.locator('button:has-text("Next")').first().click().catch(() => {});
await sleep(400);
const step4Title = await page.locator('[class*="wizard__step-title"]').first().innerText().catch(() => '');
step4Title.includes('Summary') ? ok('Wizard step 4 (Summary)') : fail('Step 4 not reached', step4Title);
// ── 14. Payment modal full flow ───────────────────────────────────────────
const payBtn = page.locator('button:has-text("Confirm & Pay")');
const hasPayBtn = await payBtn.isVisible().catch(() => false);
hasPayBtn ? ok('Confirm & Pay button in step 4') : fail('Confirm & Pay button missing');
if (hasPayBtn) {
await payBtn.click();
await sleep(500);
const payModalVisible = await page.locator('text=/Secure Payment/i').isVisible().catch(() => false);
payModalVisible ? ok('Payment modal opens') : fail('Payment modal did not open');
if (payModalVisible) {
const depositAmt = await page.locator('[class*="deposit-amt"]').innerText().catch(() => '');
depositAmt ? ok('Deposit amount displayed', depositAmt) : fail('Deposit amount missing');
// Fill card form using placeholder selectors
await page.locator('input[placeholder*="1234"]').fill('4111111111111111');
await page.locator('input[placeholder*="MM"]').fill('12/28');
await page.locator('input[type="password"]').fill('123');
await page.locator('input[placeholder*="John"]').fill('Jan Kowalski');
await sleep(300);
// Check no validation errors yet
const payDepositBtn = page.locator('[class*="pay__pay-btn"]');
await payDepositBtn.click().catch(() => {});
// Wait for PaymentModal to reach 'done' phase (~1.5s) then createReservation POST (~200ms)
// then setPaymentOpen(false) causing overlay to disappear
await page.locator('[class*="pay__overlay"]').waitFor({ state: 'hidden', timeout: 8000 }).catch(() => {});
await sleep(400);
const bookingConfirmed = await page.locator('text=/Booking Confirmed/i').isVisible().catch(() => false);
bookingConfirmed
? ok('Full booking flow — Booking Confirmed shown')
: fail('Booking Confirmed not shown after payment');
// Check receipt fields appear
const receiptVisible = await page.locator('[class*="wizard__receipt"]').isVisible().catch(() => false);
receiptVisible ? ok('Receipt block rendered') : warn('Receipt block not visible');
}
}
// ── 15. My Reservations sidebar ──────────────────────────────────────────
// Navigate fresh to see sidebar (not in done state)
await page.goto(BASE + '/dashboard');
await page.waitForLoadState('networkidle');
await sleep(600);
const myResVisible = await page.locator('text=/My Reservations/i').first().isVisible().catch(() => false);
myResVisible ? ok('My Reservations sidebar renders') : fail('My Reservations missing');
// Check it shows at least one reservation for this user (u2 has multiple)
const resItems = await page.locator('[class*="my-res__item"]').count().catch(() => 0);
resItems > 0 ? ok('My Reservations lists items', `${resItems} items`) : fail('No reservation items found');
// ── 16. Cancel from My Reservations ──────────────────────────────────────
const cancelResBtn = page.locator('[class*="my-res__cancel-btn"]').first();
const hasCancelRes = await cancelResBtn.isVisible().catch(() => false);
if (hasCancelRes) {
const countBefore = await page.locator('[class*="my-res__item"]').count();
await cancelResBtn.click();
await sleep(1000);
const cancelledBadge = await page.locator('text=Cancelled').count();
cancelledBadge >= 1 ? ok('Cancel from My Reservations updates status') : warn('Cancelled badge not found after cancel');
} else {
warn('No cancellable reservations visible in sidebar');
}
// ── 17. Dark mode toggle ─────────────────────────────────────────────────
const themeBtn = page.locator('[aria-label*="dark"],[aria-label*="light"],[aria-label*="theme"]').first();
const hasTheme = await themeBtn.isVisible().catch(() => false);
if (hasTheme) {
await themeBtn.click();
await sleep(300);
const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'));
theme === 'dark' ? ok('Dark mode toggle works') : warn('data-theme after toggle', theme ?? 'null');
} else {
warn('Theme toggle not found');
}
// ── 18. Protected route (client blocked from /admin) ─────────────────────
await page.goto(BASE + '/admin');
await sleep(600);
page.url().includes('/dashboard')
? ok('Client blocked from /admin → redirected to /dashboard')
: fail('Client could access /admin', page.url());
// ── Errors ────────────────────────────────────────────────────────────────
if (pageErrors.length > 0) {
warn('Browser console errors during test', pageErrors.slice(0, 3).join(' | '));
}
await browser.close();
console.log('\n' + '═'.repeat(62));
console.log(' MANUAL TEST RESULTS — ' + new Date().toISOString().slice(0,19));
console.log('═'.repeat(62));
results.forEach((r) => console.log(' ' + r));
const passed = results.filter((r) => r.startsWith('PASS')).length;
const failed = results.filter((r) => r.startsWith('FAIL')).length;
const warned = results.filter((r) => r.startsWith('WARN')).length;
console.log('═'.repeat(62));
console.log(` ${passed} passed · ${warned} warnings · ${failed} failed`);
console.log('═'.repeat(62));
if (failed > 0) process.exit(1);
}
run().catch((err) => { console.error(err); process.exit(1); });