towerops/e2e/tests/schedules.spec.ts
2026-05-10 17:53:09 -05:00

598 lines
23 KiB
TypeScript

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();
}
}
});
});
});