mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
1
e2e/.auth/.gitignore
vendored
Normal file
1
e2e/.auth/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.json
|
||||
56
e2e/agent-login.spec.ts
Normal file
56
e2e/agent-login.spec.ts
Normal 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
155
e2e/agent-smoke.spec.ts
Normal 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
22
e2e/auth.setup.ts
Normal 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
15
e2e/helpers.ts
Normal 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);
|
||||
}
|
||||
121
e2e/supervisor-smoke.spec.ts
Normal file
121
e2e/supervisor-smoke.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user