import { test, expect } from '@playwright/test'; test.describe('On-Call Schedules & Escalation Policies', () => { let consoleErrors: string[]; let pageErrors: Error[]; test.beforeEach(async ({ page }) => { consoleErrors = []; pageErrors = []; page.on('console', msg => { if (msg.type() === 'error') { consoleErrors.push(msg.text()); } }); page.on('pageerror', error => { pageErrors.push(error); }); }); test.afterEach(async () => { if (consoleErrors.length > 0) { console.log('Console errors:', consoleErrors); } if (pageErrors.length > 0) { console.log('Page errors:', pageErrors); } }); test.describe('Smoke Tests', () => { test('schedules index loads without errors', async ({ page }) => { const response = await page.goto('/schedules'); expect(response?.status()).toBeLessThan(500); await page.waitForTimeout(1000); await expect(page.locator('body')).toBeVisible(); }); test('new schedule form loads without errors', async ({ page }) => { const response = await page.goto('/schedules/new'); expect(response?.status()).toBeLessThan(500); await page.waitForTimeout(1000); await expect(page.locator('body')).toBeVisible(); }); test('new escalation policy form loads without errors', async ({ page }) => { const response = await page.goto('/schedules/escalation-policies/new'); expect(response?.status()).toBeLessThan(500); await page.waitForTimeout(1000); await expect(page.locator('body')).toBeVisible(); }); test('schedules page maintains socket connection', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(2000); const hasSocketError = consoleErrors.some(err => err.includes('socket') || err.includes('Jason') || err.includes('encode') ); expect(hasSocketError).toBe(false); }); }); test.describe('Navigation', () => { test('nav bar shows Schedules link', async ({ page }) => { await page.goto('/dashboard'); await page.waitForTimeout(500); const schedulesLink = page.locator('a[href="/schedules"]').first(); if (await schedulesLink.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(schedulesLink).toBeVisible(); } }); test('can navigate to schedules page from nav', async ({ page }) => { await page.goto('/dashboard'); await page.waitForTimeout(1000); const schedulesLink = page.locator('a[href="/schedules"]').first(); if (await schedulesLink.isVisible({ timeout: 2000 }).catch(() => false)) { await page.goto('/schedules'); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules/); } }); }); test.describe('Schedules Tab', () => { test('schedules page shows tabs for Schedules and Escalation Policies', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const schedulesTab = page.getByText('Schedules', { exact: true }).first(); const policiesTab = page.getByText('Escalation Policies', { exact: true }).first(); await expect(schedulesTab).toBeVisible(); await expect(policiesTab).toBeVisible(); }); test('schedules tab shows empty state or schedule cards', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); // Either shows empty state or a list of schedule cards const emptyState = page.getByText('No schedules'); const scheduleLink = page.locator('a[href^="/schedules/"]').first(); const hasEmptyState = await emptyState.isVisible({ timeout: 2000 }).catch(() => false); const hasCards = await scheduleLink.isVisible({ timeout: 2000 }).catch(() => false); expect(hasEmptyState || hasCards).toBe(true); }); test('schedules tab renders date navigation controls', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); // Today / prev / next buttons + range selector await expect(page.getByRole('button', { name: 'Today' })).toBeVisible(); await expect(page.locator('button[phx-click="prev"]')).toBeVisible(); await expect(page.locator('button[phx-click="next"]')).toBeVisible(); await expect(page.locator('select[name="range"]')).toBeVisible(); }); test('schedules tab renders the search input', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const searchInput = page.locator('input[name="search"]'); await expect(searchInput).toBeVisible(); }); test('can navigate to new schedule form', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(1000); const newButton = page.locator('a[href="/schedules/new"]').first(); if (await newButton.isVisible({ timeout: 2000 }).catch(() => false)) { await page.goto('/schedules/new'); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules\/new/); } }); test('new schedule form has required fields', async ({ page }) => { await page.goto('/schedules/new'); await page.waitForTimeout(500); // Check for name input const nameInput = page.locator('input[name*="name"]').first(); await expect(nameInput).toBeVisible(); // Check for timezone input const timezoneInput = page.locator('input[name*="timezone"]').first(); await expect(timezoneInput).toBeVisible(); // Check for save button const saveButton = page.getByText('Save Schedule'); await expect(saveButton).toBeVisible(); }); test('can create a new schedule', async ({ page }) => { await page.goto('/schedules/new'); await page.waitForTimeout(500); const nameInput = page.locator('input[name*="name"]').first(); const timezoneInput = page.locator('input[name*="timezone"]').first(); if (await nameInput.isVisible({ timeout: 2000 }).catch(() => false)) { const scheduleName = `E2E Test Schedule ${Date.now()}`; await nameInput.fill(scheduleName); if (await timezoneInput.isVisible({ timeout: 1000 }).catch(() => false)) { await timezoneInput.fill('America/Chicago'); } const saveButton = page.getByText('Save Schedule'); if (await saveButton.isVisible({ timeout: 2000 }).catch(() => false)) { await saveButton.click(); // May redirect to show page or stay on form (validation errors) const redirected = await page .waitForURL(/\/schedules\/[a-f0-9-]+$/, { timeout: 5000 }) .then(() => true) .catch(() => false); if (redirected) { await expect(page).toHaveURL(/\/schedules\/[a-f0-9-]+$/); } } } }); test('new schedule form has back link to index', async ({ page }) => { await page.goto('/schedules/new'); await page.waitForTimeout(500); // Back arrow link const backLink = page.locator('a[href="/schedules"]').first(); await expect(backLink).toBeVisible(); }); }); test.describe('Schedule Show Page', () => { test('schedule show page displays schedule details', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(1000); const scheduleRow = page.locator('a[href^="/schedules/"]').nth(0); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForURL(/\/schedules\/[a-f0-9-]+/, { timeout: 5000 }).catch(() => {}); // Should show schedule name and on-call status (may not exist in empty schedule) const onCallSection = page.getByText('Currently On-Call'); if (await onCallSection.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(onCallSection).toBeVisible(); } // Should show layers section const layersSection = page.getByText('Layers', { exact: true }); if (await layersSection.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(layersSection).toBeVisible(); } // Should show overrides section const overridesSection = page.getByText('Overrides', { exact: true }); if (await overridesSection.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(overridesSection).toBeVisible(); } } }); test('schedule show page has edit and delete buttons', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const scheduleRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForTimeout(500); const editLink = page.getByText('Edit', { exact: true }); const deleteButton = page.getByText('Delete', { exact: true }); if (await editLink.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(editLink).toBeVisible(); } if (await deleteButton.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(deleteButton).toBeVisible(); } } }); test('can navigate to edit schedule from show page', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const scheduleRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForTimeout(500); const editLink = page.locator('a[href*="/edit"]').first(); if (await editLink.isVisible({ timeout: 2000 }).catch(() => false)) { await editLink.click(); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules\/[a-f0-9-]+\/edit/); } } }); test('back button on show page goes to index', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const scheduleRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForTimeout(500); const backLink = page.locator('a[href="/schedules"]').first(); if (await backLink.isVisible({ timeout: 2000 }).catch(() => false)) { await backLink.click(); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules$/); } } }); test('can toggle the Add Layer form', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const scheduleRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForTimeout(500); const addLayerButton = page.getByText('Add Layer', { exact: true }).first(); if (await addLayerButton.isVisible({ timeout: 2000 }).catch(() => false)) { // Click to show the form await addLayerButton.click(); await page.waitForTimeout(500); // Layer form should now be visible with fields const layerNameInput = page.locator('input[name*="layer"][name*="name"]').first(); const formVisible = await layerNameInput.isVisible({ timeout: 2000 }).catch(() => false); if (formVisible) { await expect(layerNameInput).toBeVisible(); // Click cancel to hide const cancelButton = page.locator('button:has-text("Cancel")').first(); if (await cancelButton.isVisible({ timeout: 2000 }).catch(() => false)) { await cancelButton.click(); await page.waitForTimeout(500); } } } } }); test('can toggle the Add Override form', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const scheduleRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await scheduleRow.isVisible({ timeout: 2000 }).catch(() => false)) { await scheduleRow.click(); await page.waitForTimeout(500); const addOverrideButton = page.getByText('Add Override', { exact: true }).first(); if (await addOverrideButton.isVisible({ timeout: 2000 }).catch(() => false)) { // Click to show the form await addOverrideButton.click(); await page.waitForTimeout(500); // Override form should have datetime fields const startTimeInput = page.locator('input[name*="start_time"]').first(); const formVisible = await startTimeInput.isVisible({ timeout: 2000 }).catch(() => false); if (formVisible) { await expect(startTimeInput).toBeVisible(); // Click cancel to hide const cancelButton = page.locator('button:has-text("Cancel")').first(); if (await cancelButton.isVisible({ timeout: 2000 }).catch(() => false)) { await cancelButton.click(); await page.waitForTimeout(500); } } } } }); }); test.describe('Escalation Policies Tab', () => { test('can switch to escalation policies tab', async ({ page }) => { await page.goto('/schedules'); await page.waitForTimeout(500); const policiesTab = page.locator('a[href*="tab=escalation-policies"]').first(); if (await policiesTab.isVisible({ timeout: 2000 }).catch(() => false)) { await policiesTab.click(); await page.waitForTimeout(500); await expect(page).toHaveURL(/tab=escalation-policies/); } }); test('escalation policies tab shows empty state or policy list', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const emptyState = page.getByText('No escalation policies'); const policyTable = page.locator('table'); const hasEmptyState = await emptyState.isVisible({ timeout: 2000 }).catch(() => false); const hasTable = await policyTable.isVisible({ timeout: 2000 }).catch(() => false); expect(hasEmptyState || hasTable).toBe(true); }); test('can navigate to new escalation policy form', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(1000); const newButton = page.locator('a[href*="/schedules/escalation-policies/new"]').first(); if (await newButton.isVisible({ timeout: 2000 }).catch(() => false)) { await page.goto('/schedules/escalation-policies/new'); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules\/escalation-policies\/new/); } }); test('new escalation policy form has required fields', async ({ page }) => { await page.goto('/schedules/escalation-policies/new'); await page.waitForTimeout(500); const nameInput = page.locator('input[name*="name"]').first(); await expect(nameInput).toBeVisible(); // Inline repeat block (not a standalone Repeat Count label anymore) const repeatInput = page.locator('input[name*="repeat_count"]').first(); await expect(repeatInput).toBeVisible(); // Handoff notifications mode select shipped in chunk 1 const handoffSelect = page.locator('select[name*="handoff_notifications_mode"]').first(); await expect(handoffSelect).toBeVisible(); const saveButton = page.getByText('Save Policy'); await expect(saveButton).toBeVisible(); }); test('can create a new escalation policy', async ({ page }) => { await page.goto('/schedules/escalation-policies/new'); await page.waitForTimeout(500); const nameInput = page.locator('input[name*="name"]').first(); if (await nameInput.isVisible({ timeout: 2000 }).catch(() => false)) { const policyName = `E2E Test Policy ${Date.now()}`; await nameInput.fill(policyName); const saveButton = page.getByText('Save Policy'); if (await saveButton.isVisible({ timeout: 2000 }).catch(() => false)) { await saveButton.click(); // May redirect to show page or stay on form const redirected = await page .waitForURL(/\/schedules\/escalation-policies\/[a-f0-9-]+$/, { timeout: 5000 }) .then(() => true) .catch(() => false); if (redirected) { await expect(page).toHaveURL(/\/schedules\/escalation-policies\/[a-f0-9-]+$/); } } } }); test('new escalation policy form has back link', async ({ page }) => { await page.goto('/schedules/escalation-policies/new'); await page.waitForTimeout(500); const backLink = page.locator('a[href*="tab=escalation-policies"]').first(); await expect(backLink).toBeVisible(); }); }); test.describe('Escalation Policy Show Page', () => { test('policy show page displays policy details', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); // Should show the policy name const heading = page.locator('h1').first(); await expect(heading).toBeVisible(); // Should show escalation rules section const rulesSection = page.getByText('Escalation Rules'); if (await rulesSection.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(rulesSection).toBeVisible(); } } }); test('policy show page has edit and delete buttons', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); const editLink = page.getByText('Edit', { exact: true }); const deleteButton = page.getByText('Delete', { exact: true }); if (await editLink.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(editLink).toBeVisible(); } if (await deleteButton.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(deleteButton).toBeVisible(); } } }); test('can navigate to edit policy from show page', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); const editLink = page.locator('a[href*="/edit"]').first(); if (await editLink.isVisible({ timeout: 2000 }).catch(() => false)) { await editLink.click(); await page.waitForTimeout(500); await expect(page).toHaveURL(/\/schedules\/escalation-policies\/[a-f0-9-]+\/edit/); } } }); test('back button on policy show page goes to escalation policies tab', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); const backLink = page.locator('a[href*="tab=escalation-policies"]').first(); if (await backLink.isVisible({ timeout: 2000 }).catch(() => false)) { await backLink.click(); await page.waitForTimeout(500); await expect(page).toHaveURL(/tab=escalation-policies/); } } }); test('can toggle the Add Level form on policy show page', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); const addLevelButton = page.getByText('Add Level', { exact: true }).first(); if (await addLevelButton.isVisible({ timeout: 2000 }).catch(() => false)) { await addLevelButton.click(); await page.waitForTimeout(500); const timeoutInput = page.locator('input[name="timeout_minutes"]').first(); const formVisible = await timeoutInput.isVisible({ timeout: 2000 }).catch(() => false); if (formVisible) { await expect(timeoutInput).toBeVisible(); const cancelButton = page.locator('button:has-text("Cancel")').first(); if (await cancelButton.isVisible({ timeout: 2000 }).catch(() => false)) { await cancelButton.click(); await page.waitForTimeout(500); } } } } }); test('policy show page renders the REPEAT footer surfacing repeat_count', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(500); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForTimeout(500); await expect( page.getByText('If no one acknowledges, repeat this policy', { exact: false }) ).toBeVisible(); } }); test('policy show page renders the Services sidebar', async ({ page }) => { await page.goto('/schedules?tab=escalation-policies'); await page.waitForTimeout(1000); const policyRow = page.locator('tr[class*="cursor-pointer"]').first(); if (await policyRow.isVisible({ timeout: 2000 }).catch(() => false)) { await policyRow.click(); await page.waitForURL(/\/schedules\/escalation-policies\/[a-f0-9-]+/, { timeout: 5000 }).catch(() => {}); // The sidebar header reads "Services (N)" — match by prefix const servicesSidebar = page.getByText(/^Services \(\d+\)/); if (await servicesSidebar.isVisible({ timeout: 2000 }).catch(() => false)) { await expect(servicesSidebar).toBeVisible(); } } }); }); });