mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
feat: Global E2E tests, multi-agent fixes, SIP agent tracing
- 13 Global Hospital smoke tests (CC Agent + Supervisor) - Auto-unlock agent session in test setup via maint API - agent-status-toggle sends agentId from localStorage (was missing) - maint-otp-modal injects agentId from localStorage into all calls - SIP manager logs agent identity on connect/disconnect/state changes - seed-data.ts: added CC agent + marketing users, idempotent member creation, cleanup phase before seeding - .gitignore: exclude test-results/ and playwright-report/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -23,3 +23,7 @@ dist-ssr
|
|||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { fileURLToPath } from 'url';
|
|||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const authFile = path.join(__dirname, '.auth/agent.json');
|
const authFile = path.join(__dirname, '.auth/agent.json');
|
||||||
|
|
||||||
setup('login as CC Agent', async ({ page }) => {
|
setup('login as CC Agent', async ({ page, request, baseURL }) => {
|
||||||
|
// Clear any stale session lock before login
|
||||||
|
const url = baseURL ?? 'https://ramaiah.engage.healix360.net';
|
||||||
|
await request.post(`${url}/api/maint/unlock-agent`, {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-maint-otp': '400168' },
|
||||||
|
data: { agentId: 'ramaiahadmin' },
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
await page.goto('/login');
|
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="email"], input[name="email"], input[placeholder*="@"]').first().fill('ccagent@ramaiahcare.com');
|
||||||
|
|||||||
25
e2e/global-setup.ts
Normal file
25
e2e/global-setup.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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/global-agent.json');
|
||||||
|
|
||||||
|
setup('login as Global CC Agent', async ({ page, request }) => {
|
||||||
|
// Clear any stale session lock before login
|
||||||
|
await request.post('https://global.engage.healix360.net/api/maint/unlock-agent', {
|
||||||
|
headers: { 'Content-Type': 'application/json', 'x-maint-otp': '400168' },
|
||||||
|
data: { agentId: 'global' },
|
||||||
|
}).catch(() => {});
|
||||||
|
|
||||||
|
await page.goto('https://global.engage.healix360.net/login');
|
||||||
|
|
||||||
|
await page.locator('input[type="email"], input[placeholder*="@"]').first().fill('rekha.cc@globalcare.com');
|
||||||
|
await page.locator('input[type="password"]').first().fill('Global@123');
|
||||||
|
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||||
|
|
||||||
|
await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 });
|
||||||
|
await expect(page.locator('aside').first()).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
|
await page.context().storageState({ path: authFile });
|
||||||
|
});
|
||||||
120
e2e/global-smoke.spec.ts
Normal file
120
e2e/global-smoke.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Global Hospital — happy-path smoke tests.
|
||||||
|
*
|
||||||
|
* Uses saved auth state from global-setup.ts (same pattern as Ramaiah).
|
||||||
|
* Last test signs out to release the agent session.
|
||||||
|
*/
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { loginAs, waitForApp } from './helpers';
|
||||||
|
|
||||||
|
const BASE = 'https://global.engage.healix360.net';
|
||||||
|
|
||||||
|
test.describe('Global — CC Agent', () => {
|
||||||
|
|
||||||
|
test('landing page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(page.locator('aside').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Call History page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/call-history');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(page.locator('text="Call History"').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Patients page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/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(BASE + '/appointments');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(
|
||||||
|
page.locator('text=/Appointment|Schedule|No appointment/i').first(),
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('My Performance page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/my-performance');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(page.getByRole('button', { name: 'Today' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sidebar has CC Agent nav items', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/');
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Last test — sign out to release session
|
||||||
|
test('sign-out completes', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/');
|
||||||
|
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 modal.getByRole('button', { name: /sign out/i }).click();
|
||||||
|
await expect(page).toHaveURL(/\/login/, { timeout: 15_000 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Global — Supervisor', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/login');
|
||||||
|
await page.locator('input[type="email"], input[placeholder*="@"]').first().fill('dr.ramesh@globalcare.com');
|
||||||
|
await page.locator('input[type="password"]').first().fill('Global@123');
|
||||||
|
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||||
|
await expect(page).not.toHaveURL(/\/login/, { timeout: 20_000 });
|
||||||
|
await waitForApp(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('landing page loads', async ({ page }) => {
|
||||||
|
await expect(page.locator('aside').first()).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Patients page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/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(BASE + '/appointments');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(
|
||||||
|
page.locator('text=/Appointment|Schedule|No appointment/i').first(),
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Campaigns page loads', async ({ page }) => {
|
||||||
|
await page.goto(BASE + '/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(BASE + '/settings');
|
||||||
|
await waitForApp(page);
|
||||||
|
await expect(
|
||||||
|
page.locator('text=/Settings|Configuration/i').first(),
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,7 +14,7 @@ export default defineConfig({
|
|||||||
use: {
|
use: {
|
||||||
baseURL: process.env.E2E_BASE_URL ?? 'https://ramaiah.engage.healix360.net',
|
baseURL: process.env.E2E_BASE_URL ?? 'https://ramaiah.engage.healix360.net',
|
||||||
headless: true,
|
headless: true,
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'on',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
actionTimeout: 8_000,
|
actionTimeout: 8_000,
|
||||||
navigationTimeout: 15_000,
|
navigationTimeout: 15_000,
|
||||||
@@ -47,5 +47,19 @@ export default defineConfig({
|
|||||||
testMatch: /supervisor-smoke\.spec\.ts/,
|
testMatch: /supervisor-smoke\.spec\.ts/,
|
||||||
use: { browserName: 'chromium' },
|
use: { browserName: 'chromium' },
|
||||||
},
|
},
|
||||||
|
// Global Hospital — auth setup + smoke tests
|
||||||
|
{
|
||||||
|
name: 'global-setup',
|
||||||
|
testMatch: /global-setup\.ts/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'global',
|
||||||
|
dependencies: ['global-setup'],
|
||||||
|
testMatch: /global-smoke\.spec\.ts/,
|
||||||
|
use: {
|
||||||
|
storageState: path.join(__dirname, 'e2e/.auth/global-agent.json'),
|
||||||
|
browserName: 'chromium',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ async function ensureWorkspaceContext() {
|
|||||||
async function mkMember(email: string, password: string, firstName: string, lastName: string, roleName?: string): Promise<string> {
|
async function mkMember(email: string, password: string, firstName: string, lastName: string, roleName?: string): Promise<string> {
|
||||||
await ensureWorkspaceContext();
|
await ensureWorkspaceContext();
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
const existing = await gql('{ workspaceMembers { edges { node { id userEmail } } } }');
|
||||||
|
const found = existing.workspaceMembers.edges.find((e: any) => e.node.userEmail.toLowerCase() === email.toLowerCase());
|
||||||
|
if (found) {
|
||||||
|
console.log(` (exists) ${email} → ${found.node.id}`);
|
||||||
|
return found.node.id;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the user + link to workspace
|
// Create the user + link to workspace
|
||||||
await gql(
|
await gql(
|
||||||
`mutation($email: String!, $password: String!, $workspaceId: UUID!, $workspaceInviteHash: String!) {
|
`mutation($email: String!, $password: String!, $workspaceId: UUID!, $workspaceInviteHash: String!) {
|
||||||
@@ -99,11 +107,37 @@ async function mkMember(email: string, password: string, firstName: string, last
|
|||||||
return memberId;
|
return memberId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearAll() {
|
||||||
|
// Delete in reverse dependency order
|
||||||
|
const entities = ['followUp', 'leadActivity', 'call', 'appointment', 'lead', 'patient', 'doctorVisitSlot', 'doctor', 'campaign', 'clinic'];
|
||||||
|
for (const entity of entities) {
|
||||||
|
const cap = entity[0].toUpperCase() + entity.slice(1);
|
||||||
|
try {
|
||||||
|
const data = await gql(`{ ${entity}s(first: 100) { edges { node { id } } } }`);
|
||||||
|
const ids: string[] = data[`${entity}s`].edges.map((e: any) => e.node.id);
|
||||||
|
if (ids.length === 0) { console.log(` ${cap}: 0 records`); continue; }
|
||||||
|
for (const id of ids) {
|
||||||
|
await gql(`mutation { delete${cap}(id: "${id}") { id } }`);
|
||||||
|
}
|
||||||
|
console.log(` ${cap}: deleted ${ids.length}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(` ${cap}: skip (${err.message?.slice(0, 60)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('🌱 Seeding Helix Engage demo data...\n');
|
console.log('🌱 Seeding Helix Engage demo data...\n');
|
||||||
await auth();
|
await auth();
|
||||||
console.log('✅ Auth OK\n');
|
console.log('✅ Auth OK\n');
|
||||||
|
|
||||||
|
// Clean slate — remove all existing entity data (not users)
|
||||||
|
console.log('🧹 Clearing existing data...');
|
||||||
|
await clearAll();
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
await auth();
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// CLINICS (needed for doctor visit slots)
|
// CLINICS (needed for doctor visit slots)
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
@@ -136,6 +170,25 @@ async function main() {
|
|||||||
|
|
||||||
await auth();
|
await auth();
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
// CALL CENTER & MARKETING STAFF
|
||||||
|
//
|
||||||
|
// CC agents (HelixEngage User role) handle inbound/outbound calls.
|
||||||
|
// Marketing executives and supervisors use HelixEngage Supervisor role.
|
||||||
|
// Email domain uses globalcare.com to match the deployment.
|
||||||
|
// ═══════════════════════════════════════════
|
||||||
|
console.log('📞 Call center & marketing staff');
|
||||||
|
const wmRekha = await mkMember('rekha.cc@globalcare.com', 'Global@123', 'Rekha', 'Nair', 'HelixEngage User');
|
||||||
|
console.log(` Rekha (CC Agent): ${wmRekha}`);
|
||||||
|
const wmGanesh = await mkMember('ganesh.cc@globalcare.com', 'Global@123', 'Ganesh', 'Iyer', 'HelixEngage User');
|
||||||
|
console.log(` Ganesh (CC Agent): ${wmGanesh}`);
|
||||||
|
const wmSanjay = await mkMember('sanjay.marketing@globalcare.com', 'Global@123', 'Sanjay', 'Verma', 'HelixEngage Supervisor');
|
||||||
|
console.log(` Sanjay (Marketing): ${wmSanjay}`);
|
||||||
|
const wmRamesh = await mkMember('dr.ramesh@globalcare.com', 'Global@123', 'Ramesh', 'Gupta', 'HelixEngage Supervisor');
|
||||||
|
console.log(` Dr. Ramesh (Supervisor): ${wmRamesh}\n`);
|
||||||
|
|
||||||
|
await auth();
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
// DOCTOR WORKSPACE MEMBERS
|
// DOCTOR WORKSPACE MEMBERS
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ export const AgentStatusToggle = ({ isRegistered, connectionStatus }: AgentStatu
|
|||||||
try {
|
try {
|
||||||
if (newStatus === 'ready') {
|
if (newStatus === 'ready') {
|
||||||
console.log('[AGENT-STATE] Changing to Ready');
|
console.log('[AGENT-STATE] Changing to Ready');
|
||||||
const res = await apiClient.post('/api/ozonetel/agent-state', { state: 'Ready' });
|
const res = await apiClient.post('/api/ozonetel/agent-state', { agentId, state: 'Ready' });
|
||||||
console.log('[AGENT-STATE] Ready response:', JSON.stringify(res));
|
console.log('[AGENT-STATE] Ready response:', JSON.stringify(res));
|
||||||
} else {
|
} else {
|
||||||
const pauseReason = newStatus === 'break' ? 'Break' : 'Training';
|
const pauseReason = newStatus === 'break' ? 'Break' : 'Training';
|
||||||
console.log(`[AGENT-STATE] Changing to Pause: ${pauseReason}`);
|
console.log(`[AGENT-STATE] Changing to Pause: ${pauseReason}`);
|
||||||
const res = await apiClient.post('/api/ozonetel/agent-state', { state: 'Pause', pauseReason });
|
const res = await apiClient.post('/api/ozonetel/agent-state', { agentId, state: 'Pause', pauseReason });
|
||||||
console.log('[AGENT-STATE] Pause response:', JSON.stringify(res));
|
console.log('[AGENT-STATE] Pause response:', JSON.stringify(res));
|
||||||
}
|
}
|
||||||
// Don't setStatus — SSE will push the real state
|
// Don't setStatus — SSE will push the real state
|
||||||
|
|||||||
@@ -59,14 +59,18 @@ export const MaintOtpModal = ({ isOpen, onOpenChange, action, preStepContent, pr
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setOtp('');
|
setOtp('');
|
||||||
} else {
|
} else {
|
||||||
// Standard sidecar endpoint
|
// Standard sidecar endpoint — include agentId from agent config
|
||||||
|
const agentCfg = localStorage.getItem('helix_agent_config');
|
||||||
|
const agentId = agentCfg ? JSON.parse(agentCfg).ozonetelAgentId : undefined;
|
||||||
|
const payload = { ...preStepPayload, ...(agentId ? { agentId } : {}) };
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}/api/maint/${action.endpoint}`, {
|
const res = await fetch(`${API_URL}/api/maint/${action.endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-maint-otp': otp,
|
'x-maint-otp': otp,
|
||||||
},
|
},
|
||||||
...(preStepPayload ? { body: JSON.stringify(preStepPayload) } : {}),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ let sipClient: SIPClient | null = null;
|
|||||||
let connected = false;
|
let connected = false;
|
||||||
let outboundPending = false;
|
let outboundPending = false;
|
||||||
let outboundActive = false;
|
let outboundActive = false;
|
||||||
|
let activeAgentId: string | null = null;
|
||||||
|
|
||||||
type StateUpdater = {
|
type StateUpdater = {
|
||||||
setConnectionStatus: (status: ConnectionStatus) => void;
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
||||||
@@ -42,6 +43,16 @@ export function connectSip(config: SIPConfig): void {
|
|||||||
sipClient.disconnect();
|
sipClient.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve agent identity for logging
|
||||||
|
try {
|
||||||
|
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
|
||||||
|
activeAgentId = agentCfg.ozonetelAgentId ?? null;
|
||||||
|
const ext = config.uri?.match(/sip:(\d+)@/)?.[1] ?? 'unknown';
|
||||||
|
console.log(`[SIP] Connecting agent=${activeAgentId} ext=${ext} ws=${config.wsServer}`);
|
||||||
|
} catch {
|
||||||
|
console.log(`[SIP] Connecting uri=${config.uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
connected = true;
|
connected = true;
|
||||||
stateUpdater?.setConnectionStatus('connecting');
|
stateUpdater?.setConnectionStatus('connecting');
|
||||||
|
|
||||||
@@ -62,7 +73,7 @@ export function connectSip(config: SIPConfig): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[SIP-MGR] State: ${state} | caller=${number ?? 'none'} | ucid=${ucid ?? 'none'} | outboundActive=${outboundActive}`);
|
console.log(`[SIP] ${activeAgentId} | state=${state} | caller=${number ?? 'none'} | ucid=${ucid ?? 'none'} | outbound=${outboundActive}`);
|
||||||
|
|
||||||
stateUpdater?.setCallState(state);
|
stateUpdater?.setCallState(state);
|
||||||
if (!outboundActive && number !== undefined) {
|
if (!outboundActive && number !== undefined) {
|
||||||
@@ -90,12 +101,13 @@ export function disconnectSip(force = false): void {
|
|||||||
console.log('[SIP-MGR] Disconnect blocked — call in progress');
|
console.log('[SIP-MGR] Disconnect blocked — call in progress');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[SIP-MGR] Disconnecting SIP' + (force ? ' (forced)' : ''));
|
console.log(`[SIP] Disconnecting agent=${activeAgentId}` + (force ? ' (forced)' : ''));
|
||||||
sipClient?.disconnect();
|
sipClient?.disconnect();
|
||||||
sipClient = null;
|
sipClient = null;
|
||||||
connected = false;
|
connected = false;
|
||||||
outboundPending = false;
|
outboundPending = false;
|
||||||
outboundActive = false;
|
outboundActive = false;
|
||||||
|
activeAgentId = null;
|
||||||
stateUpdater?.setConnectionStatus('disconnected');
|
stateUpdater?.setConnectionStatus('disconnected');
|
||||||
stateUpdater?.setCallUcid(null);
|
stateUpdater?.setCallUcid(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user