Files
BIU_System_Rezerwacji/test_rms.mjs

346 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
});
// Delete any reservations created by previous test runs (not part of seed r1r56).
const seedIds = new Set(Array.from({ length: 56 }, (_, i) => `r${i + 1}`));
const all = await fetch(`${api}/reservations`).then((r) => r.json()).catch(() => []);
await Promise.all(
all
.filter((r) => !seedIds.has(r.id))
.map((r) => fetch(`${api}/reservations/${r.id}`, { method: 'DELETE' }))
);
// 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); });