// ============================================================ // EXTENSIONS TAB — full CRUD + soft-delete + restore + search // ============================================================ // Uses the shared fixture which logs in + wires pageerror/console-error // listeners. No AI mocking needed — Extensions is pure CRUD. // // Tests against the e2e container (port 3553 on host, 3000 internal). // Each test cleans up its own rows to stay isolated. // ============================================================ const { test, expect, E2E_BASE, getAuthToken } = require('../fixtures'); // Helper — purge everything in trash, then soft-delete every active row. // Leaves the table empty for the next test. async function wipeAll(request) { const token = await getAuthToken(request); const auth = { Authorization: 'Bearer ' + token }; async function purgeTrash() { const r = await request.get(E2E_BASE + '/api/extensions?trash=1', { headers: auth }); const d = await r.json(); for (const item of (d.items || [])) { await request.delete(E2E_BASE + '/api/extensions/' + item.id + '/purge', { headers: auth }); } } await purgeTrash(); // first purge anything already in trash const r = await request.get(E2E_BASE + '/api/extensions', { headers: auth }); const d = await r.json(); for (const item of (d.items || [])) { await request.delete(E2E_BASE + '/api/extensions/' + item.id, { headers: auth }); } await purgeTrash(); // now purge the freshly-trashed rows } test.describe('Extensions — CRUD', () => { test.beforeEach(async ({ request }) => { await wipeAll(request); }); test.afterAll(async ({ request }) => { await wipeAll(request); }); async function openTab(page) { await page.goto(E2E_BASE + '/'); await page.waitForSelector('button.tab-btn', { timeout: 15000 }); const vp = page.viewportSize(); if (vp && vp.width <= 768) { await page.click('#btn-menu-toggle').catch(() => {}); } await page.click('button.tab-btn[data-tab="extensions"]'); // Wait for component to load await page.waitForFunction(() => { const el = document.getElementById('extensions-tab'); return el && el.classList.contains('active') && el.innerHTML.trim().length > 100; }, { timeout: 15000 }); // Wait for first load to resolve (spinner text goes away) await page.waitForFunction(() => { const list = document.getElementById('ext-list'); return list && !list.innerHTML.includes('Loading'); }, { timeout: 10000 }); } async function fillForm(page, { location, name, number, type = 'extension', notes = '' }) { await page.click('#ext-add-btn'); await page.fill('#ext-location', location); await page.fill('#ext-name', name); await page.fill('#ext-number', number); await page.selectOption('#ext-type', type); if (notes) await page.fill('#ext-notes', notes); await page.click('#ext-save-btn'); // Form hides after save await expect(page.locator('#ext-form-wrap')).toHaveClass(/hidden/, { timeout: 5000 }); } test('empty state shown before any entries', async ({ authedPage: _, page }) => { await openTab(page); await expect(page.locator('#ext-list')).toContainText(/No entries yet|click Add/i); }); test('add an extension — appears in list grouped by location + type', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Main Hospital', name: 'Nursery', number: '5866', type: 'extension' }); // Location group header await expect(page.locator('#ext-list')).toContainText('Main Hospital'); // Type subheader await expect(page.locator('#ext-list')).toContainText(/Extensions/i); // Card content await expect(page.locator('.ext-card')).toContainText('5866'); await expect(page.locator('.ext-card')).toContainText('Nursery'); }); test('add a pager — routed to pagers subsection', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Clinic A', name: 'On-call', number: '1234', type: 'pager' }); await expect(page.locator('#ext-list')).toContainText('Clinic A'); await expect(page.locator('#ext-list')).toContainText(/Pagers/i); await expect(page.locator('.ext-card')).toContainText('1234'); }); test('edit an extension — changes persist', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Main Hospital', name: 'Old Name', number: '1111', type: 'extension' }); await page.locator('.ext-card button[data-ext-action="edit"]').first().click(); await expect(page.locator('#ext-form-wrap')).not.toHaveClass(/hidden/); await page.fill('#ext-name', 'New Name'); await page.fill('#ext-number', '2222'); await page.click('#ext-save-btn'); await expect(page.locator('#ext-form-wrap')).toHaveClass(/hidden/, { timeout: 5000 }); await expect(page.locator('.ext-card')).toContainText('New Name'); await expect(page.locator('.ext-card')).toContainText('2222'); await expect(page.locator('.ext-card')).not.toContainText('Old Name'); }); test('search — filter by location', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Main Hospital', name: 'Dept A', number: '1001', type: 'extension' }); await fillForm(page, { location: 'Clinic B', name: 'Dept B', number: '2002', type: 'extension' }); // Initially both visible await expect(page.locator('.ext-card')).toHaveCount(2); // Search by substring of first location await page.fill('#ext-search', 'Main'); // Debounce is 200ms; wait for the filter to apply await page.waitForTimeout(400); await expect(page.locator('.ext-card')).toHaveCount(1); await expect(page.locator('.ext-card')).toContainText('Dept A'); // Clear search — both visible again await page.fill('#ext-search', ''); await page.waitForTimeout(400); await expect(page.locator('.ext-card')).toHaveCount(2); }); test('search — filter by number', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'A', number: '5866', type: 'extension' }); await fillForm(page, { location: 'Loc', name: 'B', number: '1234', type: 'pager' }); await page.fill('#ext-search', '586'); await page.waitForTimeout(400); await expect(page.locator('.ext-card')).toHaveCount(1); await expect(page.locator('.ext-card')).toContainText('5866'); }); test('soft-delete — moves to trash, confirm dialog required', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'To delete', number: '9999', type: 'extension' }); await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); // In-app modal — click confirm await page.click('#confirm-modal-ok'); // Gone from active list await expect(page.locator('#ext-list')).toContainText(/No entries yet/i, { timeout: 5000 }); // Visible in trash view await page.click('#ext-trash-btn'); await expect(page.locator('#ext-mode-banner')).not.toHaveClass(/hidden/); await expect(page.locator('.ext-card')).toContainText('To delete'); }); test('restore from trash — reappears in active', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'Bounceback', number: '5555', type: 'extension' }); await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); await page.click('#confirm-modal-ok'); await page.click('#ext-trash-btn'); await expect(page.locator('.ext-card')).toContainText('Bounceback'); await page.locator('.ext-card button[data-ext-action="restore"]').first().click(); // Back-to-active button await page.click('#ext-back-active'); await expect(page.locator('.ext-card')).toContainText('Bounceback'); }); test('purge from trash — permanent, second confirm', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'Gone forever', number: '7777', type: 'extension' }); await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); await page.click('#confirm-modal-ok'); await page.click('#ext-trash-btn'); await expect(page.locator('.ext-card')).toContainText('Gone forever'); await page.locator('.ext-card button[data-ext-action="purge"]').first().click(); await page.click('#confirm-modal-ok'); // Trash is empty await expect(page.locator('#ext-list')).toContainText(/Trash is empty/i, { timeout: 5000 }); // Not recoverable — back-to-active view has nothing await page.click('#ext-back-active'); await expect(page.locator('#ext-list')).toContainText(/No entries yet/i); }); test('cancel dialog — keeps item (not deleted)', async ({ authedPage: _, page }) => { await openTab(page); await fillForm(page, { location: 'Loc', name: 'Stays', number: '8888', type: 'extension' }); // User cancels the modal await page.locator('.ext-card button[data-ext-action="delete"]').first().click(); await page.click('#confirm-modal-cancel'); // Still there await expect(page.locator('.ext-card')).toContainText('Stays'); }); test('cancel button closes form without saving', async ({ authedPage: _, page }) => { await openTab(page); await page.click('#ext-add-btn'); await page.fill('#ext-location', 'Should not save'); await page.fill('#ext-name', 'x'); await page.fill('#ext-number', '0'); await page.click('#ext-cancel-btn'); await expect(page.locator('#ext-form-wrap')).toHaveClass(/hidden/); // Nothing was created await expect(page.locator('#ext-list')).toContainText(/No entries yet/i); }); test('form validates required fields', async ({ authedPage: _, page }) => { await openTab(page); await page.click('#ext-add-btn'); // Click save with empty fields await page.click('#ext-save-btn'); await expect(page.locator('#ext-form-status')).toContainText(/required/i); // Form stays open await expect(page.locator('#ext-form-wrap')).not.toHaveClass(/hidden/); }); });