feat: add E2E smoke tests, architecture docs, and operations runbook

- 27 Playwright E2E tests covering login (3 roles), CC Agent pages
  (call desk, call history, patients, appointments, my performance,
  sidebar, sign-out), and Supervisor pages (all 11 pages + sidebar)
- Tests run against live EC2 at ramaiah.engage.healix360.net
- Last test completes sign-out to release agent session for next run
- Architecture doc with updated Mermaid diagram including telephony
  dispatcher, service discovery, and multi-tenant topology
- Operations runbook with SSH access (VPS + EC2), accounts, container
  reference, deploy steps, Redis ops, and troubleshooting guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 20:54:20 +05:30
parent a1598716ee
commit 1cdb7fe9e7
11 changed files with 1056 additions and 0 deletions

1
e2e/.auth/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.json

56
e2e/agent-login.spec.ts Normal file
View File

@@ -0,0 +1,56 @@
/**
* Login feature tests — covers multiple roles.
*
* These run WITHOUT saved auth state (fresh browser).
*/
import { test, expect } from '@playwright/test';
import { waitForApp } from './helpers';
const SUPERVISOR = { email: 'supervisor@ramaiahcare.com', password: 'MrRamaiah@2026' };
test.describe('Login', () => {
test('login page renders with branding', async ({ page }) => {
await page.goto('/login');
await expect(page.locator('img[alt]').first()).toBeVisible();
await expect(page.locator('h1').first()).toBeVisible();
await expect(page.locator('form')).toBeVisible();
await expect(page.locator('input[type="email"], input[placeholder*="@"]').first()).toBeVisible();
await expect(page.locator('input[type="password"]').first()).toBeVisible();
await expect(page.getByRole('button', { name: 'Sign in', exact: true })).toBeVisible();
});
test('invalid credentials show error', async ({ page }) => {
await page.goto('/login');
await page.locator('input[type="email"], input[placeholder*="@"]').first().fill('bad@bad.com');
await page.locator('input[type="password"]').first().fill('wrongpassword');
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await expect(
page.locator('text=/not found|invalid|incorrect|failed|error|unauthorized/i').first(),
).toBeVisible({ timeout: 10_000 });
await expect(page).toHaveURL(/\/login/);
});
test('Supervisor login → lands on app', async ({ page }) => {
await page.goto('/login');
await page.locator('input[type="email"], input[placeholder*="@"]').first().fill(SUPERVISOR.email);
await page.locator('input[type="password"]').first().fill(SUPERVISOR.password);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 });
await waitForApp(page);
// Sidebar should be visible
await expect(page.locator('aside').first()).toBeVisible();
});
test('unauthenticated user redirected to login', async ({ page }) => {
await page.context().clearCookies();
await page.goto('/patients');
await expect(page).toHaveURL(/\/login/, { timeout: 10_000 });
});
});

155
e2e/agent-smoke.spec.ts Normal file
View File

@@ -0,0 +1,155 @@
/**
* CC Agent — happy-path smoke tests.
*
* Role: cc-agent (ccagent@ramaiahcare.com)
* Landing: / → Call Desk
* Pages: Call Desk, Call History, Patients, Appointments, My Performance
*/
import { test, expect } from '@playwright/test';
import { waitForApp } from './helpers';
test.describe('CC Agent Smoke', () => {
test('lands on Call Desk after login', async ({ page }) => {
await page.goto('/');
await waitForApp(page);
await expect(page.locator('aside').first()).toContainText(/Call Center/i);
});
test('Call Desk page loads', async ({ page }) => {
await page.goto('/');
await waitForApp(page);
// Call Desk is the landing — just verify we're not on an error page
await expect(page.locator('aside').first()).toContainText(/Call Desk/i);
});
test('Call History page loads', async ({ page }) => {
await page.goto('/call-history');
await waitForApp(page);
// Should show "Call History" title whether or not there are calls
await expect(page.locator('text="Call History"').first()).toBeVisible();
// Filter dropdown present
await expect(page.locator('text=/All Calls/i').first()).toBeVisible();
});
test('Patients page loads with search', async ({ page }) => {
await page.goto('/patients');
await waitForApp(page);
const search = page.getByPlaceholder(/search/i).or(page.getByLabel(/search/i));
await expect(search.first()).toBeVisible();
});
test('Patients search filters results', async ({ page }) => {
await page.goto('/patients');
await waitForApp(page);
const search = page.getByPlaceholder(/search/i).or(page.getByLabel(/search/i));
await search.first().fill('zzz-nonexistent-patient');
await waitForApp(page);
// Should show empty state
const noResults = page.locator('text=/no patient|not found|no results/i');
const isEmpty = await noResults.isVisible({ timeout: 5_000 }).catch(() => false);
expect(isEmpty).toBe(true);
});
test('Appointments page loads', async ({ page }) => {
await page.goto('/appointments');
await waitForApp(page);
await expect(
page.locator('text=/Appointment|Schedule|No appointment/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('My Performance loads with date controls', async ({ page }) => {
// Intercept API to verify agentId is sent
const apiHit = page.waitForRequest(
(r) => r.url().includes('/api/ozonetel/performance') && r.url().includes('agentId='),
{ timeout: 15_000 },
);
await page.goto('/my-performance');
const req = await apiHit;
const url = new URL(req.url());
expect(url.searchParams.get('agentId')?.length).toBeGreaterThan(0);
await waitForApp(page);
await expect(page.getByRole('button', { name: 'Today' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Yesterday' })).toBeVisible();
// Either KPI data or empty state
await expect(
page.locator('text=/Total Calls|No performance data/i').first(),
).toBeVisible();
});
test('sidebar has all CC Agent nav items', async ({ page }) => {
await page.goto('/');
await waitForApp(page);
const sidebar = page.locator('aside').first();
for (const item of ['Call Desk', 'Call History', 'Patients', 'Appointments', 'My Performance']) {
await expect(sidebar.locator(`text="${item}"`)).toBeVisible();
}
});
test('sign-out shows confirmation modal and cancel keeps session', async ({ page }) => {
await page.goto('/');
await waitForApp(page);
const sidebar = page.locator('aside').first();
const accountArea = sidebar.locator('[class*="account"], [class*="avatar"]').last();
if (await accountArea.isVisible()) {
await accountArea.click();
}
const signOutBtn = page.locator('button, [role="menuitem"]').filter({ hasText: /sign out/i }).first();
if (await signOutBtn.isVisible({ timeout: 5_000 }).catch(() => false)) {
await signOutBtn.click();
const modal = page.locator('[role="dialog"]');
await expect(modal).toBeVisible({ timeout: 5_000 });
await expect(modal).toContainText(/sign out/i);
// Cancel — should stay logged in
await modal.getByRole('button', { name: /cancel/i }).click();
await expect(modal).not.toBeVisible();
await expect(page).not.toHaveURL(/\/login/);
}
});
// MUST be the last test — completes sign-out so the agent session is
// released and the next test run won't hit "already logged in".
test('sign-out completes and redirects to login', async ({ page }) => {
await page.goto('/');
await waitForApp(page);
const sidebar = page.locator('aside').first();
const accountArea = sidebar.locator('[class*="account"], [class*="avatar"]').last();
if (await accountArea.isVisible()) {
await accountArea.click();
}
const signOutBtn = page.locator('button, [role="menuitem"]').filter({ hasText: /sign out/i }).first();
if (await signOutBtn.isVisible({ timeout: 5_000 }).catch(() => false)) {
await signOutBtn.click();
const modal = page.locator('[role="dialog"]');
await expect(modal).toBeVisible({ timeout: 5_000 });
// Confirm sign out
await modal.getByRole('button', { name: /sign out/i }).click();
// Should redirect to login
await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
}
});
});

22
e2e/auth.setup.ts Normal file
View File

@@ -0,0 +1,22 @@
import { test as setup, expect } from '@playwright/test';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const authFile = path.join(__dirname, '.auth/agent.json');
setup('login as CC Agent', async ({ page }) => {
await page.goto('/login');
await page.locator('input[type="email"], input[name="email"], input[placeholder*="@"]').first().fill('ccagent@ramaiahcare.com');
await page.locator('input[type="password"]').first().fill('CcRamaiah@2026');
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
// Should land on Call Desk (/ for cc-agent role)
await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 });
// Sidebar should be visible
await expect(page.locator('aside').first()).toBeVisible({ timeout: 10_000 });
await page.context().storageState({ path: authFile });
});

15
e2e/helpers.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Page, expect } from '@playwright/test';
export async function waitForApp(page: Page) {
await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => {});
await page.waitForTimeout(300);
}
export async function loginAs(page: Page, email: string, password: string) {
await page.goto('/login');
await page.locator('input[type="email"], input[name="email"], input[placeholder*="@"]').first().fill(email);
await page.locator('input[type="password"]').first().fill(password);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 });
await waitForApp(page);
}

View File

@@ -0,0 +1,121 @@
/**
* Supervisor / Admin — happy-path smoke tests.
*
* Role: admin (supervisor@ramaiahcare.com)
* Landing: / → Dashboard
* Pages: Dashboard, Team Performance, Live Monitor,
* Leads, Patients, Appointments, Call Log,
* Call Recordings, Missed Calls, Campaigns, Settings
*/
import { test, expect } from '@playwright/test';
import { loginAs, waitForApp } from './helpers';
const EMAIL = 'supervisor@ramaiahcare.com';
const PASSWORD = 'MrRamaiah@2026';
test.describe('Supervisor Smoke', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, EMAIL, PASSWORD);
});
test('lands on Dashboard after login', async ({ page }) => {
await expect(page.locator('aside').first()).toBeVisible();
// Verify we're authenticated and on the app
await expect(page).not.toHaveURL(/\/login/);
});
test('Team Performance loads', async ({ page }) => {
await page.goto('/team-performance');
await waitForApp(page);
await expect(
page.locator('text=/Team|Performance|Agent|No data/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Live Call Monitor loads', async ({ page }) => {
await page.goto('/live-monitor');
await waitForApp(page);
await expect(
page.locator('text=/Live|Monitor|Active|No active/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Leads page loads', async ({ page }) => {
await page.goto('/leads');
await waitForApp(page);
await expect(
page.locator('text=/Lead|No leads/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Patients page loads', async ({ page }) => {
await page.goto('/patients');
await waitForApp(page);
const search = page.getByPlaceholder(/search/i).or(page.getByLabel(/search/i));
await expect(search.first()).toBeVisible();
});
test('Appointments page loads', async ({ page }) => {
await page.goto('/appointments');
await waitForApp(page);
await expect(
page.locator('text=/Appointment|Schedule|No appointment/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Call Log page loads', async ({ page }) => {
await page.goto('/call-history');
await waitForApp(page);
await expect(page.locator('text="Call History"').first()).toBeVisible();
});
test('Call Recordings page loads', async ({ page }) => {
await page.goto('/call-recordings');
await waitForApp(page);
await expect(
page.locator('text=/Recording|No recording/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Missed Calls page loads', async ({ page }) => {
await page.goto('/missed-calls');
await waitForApp(page);
await expect(
page.locator('text=/Missed|No missed/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Campaigns page loads', async ({ page }) => {
await page.goto('/campaigns');
await waitForApp(page);
await expect(
page.locator('text=/Campaign|No campaign/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('Settings page loads', async ({ page }) => {
await page.goto('/settings');
await waitForApp(page);
await expect(
page.locator('text=/Settings|Configuration/i').first(),
).toBeVisible({ timeout: 10_000 });
});
test('sidebar has expected nav items', async ({ page }) => {
const sidebar = page.locator('aside').first();
// Check key items — exact labels depend on the role the sidecar assigns
for (const item of ['Patients', 'Appointments', 'Campaigns']) {
await expect(sidebar.locator(`text="${item}"`)).toBeVisible();
}
});
});