598 lines
23 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|