Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/calling/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {USER_SETS} from './playwright/test-data';
dotenv.config({path: path.resolve(__dirname, '../../.env')});

const BASE_URL = process.env.PW_BASE_URL || 'https://localhost:8000';
const dummyAudioPath = path.resolve(__dirname, '../../cc_playwright/wav/dummyAudio.wav');

// Browser selection via PW_BROWSER env var: 'chrome' (default), 'firefox', 'edge', 'safari'
const PW_BROWSER = process.env.PW_BROWSER || 'chrome';
Expand All @@ -19,6 +20,7 @@ const chromiumArgs = [
'--allow-file-access-from-files', // Allow file:// protocol access
'--use-fake-ui-for-media-stream', // Auto-grant camera/mic permissions without prompt
'--use-fake-device-for-media-stream', // Use synthetic audio/video instead of real hardware
`--use-file-for-fake-audio-capture=${dummyAudioPath}`, // Feed deterministic audio into WebRTC calls
'--disable-extensions', // Prevent extensions from interfering with tests
'--disable-plugins', // Prevent plugins from interfering with tests
'--ignore-certificate-errors', // Accept self-signed certs from local dev server
Expand Down Expand Up @@ -142,18 +144,33 @@ export default defineConfig({
testMatch: USER_SETS.SET_CALL_HISTORY.testSuite,
use: {...browserOptions[PW_BROWSER], testEnv: 'int'} as any,
},
// Voicemail message tests use USER_1+USER_2, so run after call history.
{
name: 'SET_VOICEMAIL - PROD',
dependencies: ['SET_CALL_HISTORY - PROD'],
testDir: './playwright/suites',
testMatch: USER_SETS.SET_VOICEMAIL.testSuite,
use: browserOptions[PW_BROWSER],
},
{
name: 'SET_VOICEMAIL - INT',
dependencies: ['SET_CALL_HISTORY - INT'],
testDir: './playwright/suites',
testMatch: USER_SETS.SET_VOICEMAIL.testSuite,
use: {...browserOptions[PW_BROWSER], testEnv: 'int'} as any,
},

// 3-user transfer tests — waits for call history because both suites use USER_1/USER_2.
// 3-user transfer tests — waits for call history and voicemail because they use USER_1/USER_2.
{
name: 'SET_CALL_TRANSFER_CONSULT - PROD',
dependencies: ['SET_CALL - PROD', 'SET_CALL_HISTORY - PROD'],
dependencies: ['SET_CALL - PROD', 'SET_CALL_HISTORY - PROD', 'SET_VOICEMAIL - PROD'],
testDir: './playwright/suites',
testMatch: USER_SETS.SET_CALL_TRANSFER_CONSULT.testSuite,
use: browserOptions[PW_BROWSER],
},
{
name: 'SET_CALL_TRANSFER_CONSULT - INT',
dependencies: ['SET_CALL - INT', 'SET_CALL_HISTORY - INT'],
dependencies: ['SET_CALL - INT', 'SET_CALL_HISTORY - INT', 'SET_VOICEMAIL - INT'],
testDir: './playwright/suites',
testMatch: USER_SETS.SET_CALL_TRANSFER_CONSULT.testSuite,
use: {...browserOptions[PW_BROWSER], testEnv: 'int'} as any,
Expand Down
8 changes: 8 additions & 0 deletions packages/calling/playwright/constants/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export const CALLING_SELECTORS = {
CALL_HISTORY_HEADER: '#callHistoryHeaderId',
CALL_HISTORY_TABLE_BODY: '#callHistoryTableId',

// Voicemail list
VOICEMAIL_BTN: '#Voice-mail',
VOICEMAIL_TABLE: '#voicemailTable',
VOICEMAIL_TABLE_ROWS: '#voicemailTable tbody tr',
VOICEMAIL_OFFSET_INPUT: '#offset',
VOICEMAIL_OFFSET_LIMIT_INPUT: '#offsetLength',
VOICEMAIL_FETCH_LIST_BTN: '#fetchVoicemailListButton',

END_BTN: '#end',

// Call Settings
Expand Down
5 changes: 5 additions & 0 deletions packages/calling/playwright/suites/voicemail.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {voicemailTests} from '../test-groups/voicemail';

// Account roles resolved from testInfo.project.name -> USER_SETS.
// SET_VOICEMAIL: accounts[0] = USER_1 (caller), accounts[1] = USER_2 (voicemail owner).
voicemailTests();
7 changes: 7 additions & 0 deletions packages/calling/playwright/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ export const USER_SETS: Record<string, UserSet> = {
testSuite: 'call-history.spec.ts',
},

// Voicemail message E2E uses USER_1 as caller and USER_2 as voicemail owner.
SET_VOICEMAIL: {
name: 'SET_VOICEMAIL',
accounts: ['USER_1', 'USER_2'],
testSuite: 'voicemail.spec.ts',
},

// 3-user transfer tests (PROD — dedicated accounts, parallel with registration)
SET_CALL_TRANSFER_CONSULT: {
name: 'SET_CALL_TRANSFER_CONSULT',
Expand Down
184 changes: 184 additions & 0 deletions packages/calling/playwright/test-groups/voicemail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {test, expect, Page} from '@playwright/test';
import {TestManager} from '../test-manager';
import {getPhoneNumber} from '../test-data';
import {
cleanupActiveCalls,
endCallerIfStillActive,
makeCall,
waitForCallDisconnect,
waitForCallEstablished,
waitForCallerOutboundCall,
} from '../utils/call';
import {
loadSettings,
saveVoicemailSettings,
setVoicemailSendAllCalls,
setVoicemailSendBusyCalls,
setVoicemailSendUnansweredCalls,
} from '../utils/call-settings';
import {
attachVoicemailSummary,
deleteVoicemail,
expectVoicemailVisibleInCurrentUi,
expectVoicemailVisibleInUi,
fetchVoicemailPageFromUi,
getVoicemailMessageId,
getVoicemailRecords,
markVoicemailUnread,
openVoicemailList,
playVoicemailFromUi,
waitForVoicemailFromCaller,
} from '../utils/voicemail';
import {CALLING_SELECTORS} from '../constants';

const VOICEMAIL_FORWARD_TIMEOUT = 60000;
const VOICEMAIL_RECORDING_DURATION_MS = 15000;
const VOICEMAIL_SETTINGS_PROPAGATION_MS = 5000;
const VOICEMAIL_PLAYBACK_TIMEOUT_MS = 60000;

const ensureVoicemailEnabled = async (calleePage: Page) => {
const enabled = calleePage.locator(CALLING_SELECTORS.VM_ENABLED_CB);

if (!(await enabled.isChecked())) {
await enabled.check();
await saveVoicemailSettings(calleePage);
}
};

const configureVoicemailForUnansweredCalls = async (calleePage: Page) => {
await loadSettings(calleePage);
await ensureVoicemailEnabled(calleePage);
await loadSettings(calleePage);
await setVoicemailSendAllCalls(calleePage, false);
await setVoicemailSendBusyCalls(calleePage, false);
await setVoicemailSendUnansweredCalls(calleePage, true, '2');
await loadSettings(calleePage);

await expect(calleePage.locator(CALLING_SELECTORS.VM_ENABLED_CB)).toBeChecked();
await expect(calleePage.locator(CALLING_SELECTORS.VM_SEND_ALL_CB)).not.toBeChecked();
await expect(calleePage.locator(CALLING_SELECTORS.VM_SEND_BUSY_CB)).not.toBeChecked();
await expect(calleePage.locator(CALLING_SELECTORS.VM_UNANSWERED_CB)).toBeChecked();
await expect(calleePage.locator(CALLING_SELECTORS.VM_UNANSWERED_RINGS)).toHaveValue('2');
await calleePage.waitForTimeout(VOICEMAIL_SETTINGS_PROPAGATION_MS);
};

export function voicemailTests() {
test.describe('Voicemail', () => {
test.describe.configure({mode: 'serial', timeout: 900000});

let tm: TestManager;
let callerNumber: string;
let calleeNumber: string;

test.beforeAll(async ({browser}, testInfo) => {
tm = new TestManager(testInfo.project.name);
await tm.setupContext(browser, 0, {
initSDK: true,
service: 'calling',
register: true,
media: true,
});
await tm.setupContext(browser, 1, {
initSDK: true,
service: 'calling',
register: true,
media: true,
});
callerNumber = getPhoneNumber(tm.userSet.accounts[0], tm.isInt);
calleeNumber = getPhoneNumber(tm.userSet.accounts[1], tm.isInt);
});

test.afterEach(async () => {
await Promise.all([
cleanupActiveCalls(tm.getPage(tm.userSet.accounts[0])),
cleanupActiveCalls(tm.getPage(tm.userSet.accounts[1])),
]);
if (!tm.page.isClosed()) {
await tm.page.waitForTimeout(3000);
}
});

test.afterAll(async () => {
await tm.cleanup();
});

/* eslint-disable no-empty-pattern */
test('VM-CALL-001: Unanswered call creates voicemail that can be listed, played, marked read, and paginated', async ({}, testInfo) => {
const callerPage = tm.getPage(tm.userSet.accounts[0]);
const calleePage = tm.getPage(tm.userSet.accounts[1]);
let messageId: string | undefined;

await configureVoicemailForUnansweredCalls(calleePage);
const existingMessageIds = await getVoicemailRecords(calleePage, {
offset: 0,
limit: 50,
refresh: true,
}).then((records) => records.map(getVoicemailMessageId).filter(Boolean));
const startedAt = new Date(Date.now() - 5000);

try {
await makeCall(callerPage, calleeNumber);
await waitForCallerOutboundCall(callerPage);

await expect(calleePage.locator(CALLING_SELECTORS.INCOMING_ANSWER_BTN)).toBeEnabled({
timeout: 30000,
});
await expect(calleePage.locator(CALLING_SELECTORS.INCOMING_ANSWER_BTN)).toBeDisabled({
timeout: VOICEMAIL_FORWARD_TIMEOUT,
});

await waitForCallEstablished(callerPage, VOICEMAIL_FORWARD_TIMEOUT);
await callerPage.waitForTimeout(VOICEMAIL_RECORDING_DURATION_MS);
await endCallerIfStillActive(callerPage);
await waitForCallDisconnect(callerPage, VOICEMAIL_FORWARD_TIMEOUT).catch(() => {});

const voicemailRecord = await waitForVoicemailFromCaller(
calleePage,
callerNumber,
startedAt,
existingMessageIds
);
messageId = getVoicemailMessageId(voicemailRecord);
await attachVoicemailSummary(testInfo, 'created', voicemailRecord);

await markVoicemailUnread(calleePage, messageId);
await openVoicemailList(calleePage);
await expectVoicemailVisibleInUi(calleePage, voicemailRecord);

await playVoicemailFromUi(calleePage, messageId, VOICEMAIL_PLAYBACK_TIMEOUT_MS);
const readRecord = await getVoicemailRecords(calleePage, {offset: 0, limit: 20}).then(
(records) => records.find((record) => getVoicemailMessageId(record) === messageId)
);
if (!readRecord) {
throw new Error(`Expected voicemail ${messageId} after play`);
}
await attachVoicemailSummary(testInfo, 'read-after-play', readRecord);

const firstPageRecords = await getVoicemailRecords(calleePage, {
offset: 0,
limit: 1,
refresh: true,
});

expect(firstPageRecords.length).toBe(1);
const paginationRows = await fetchVoicemailPageFromUi(calleePage, 0, 1);

expect(paginationRows.length).toBe(1);
await expectVoicemailVisibleInCurrentUi(calleePage, firstPageRecords[0]);
await playVoicemailFromUi(
calleePage,
getVoicemailMessageId(firstPageRecords[0]),
VOICEMAIL_PLAYBACK_TIMEOUT_MS
);
} finally {
await cleanupActiveCalls(callerPage);
await loadSettings(calleePage).catch(() => {});
await setVoicemailSendUnansweredCalls(calleePage, false).catch(() => {});

if (messageId) {
await deleteVoicemail(calleePage, messageId);
}
}
});
});
}
9 changes: 7 additions & 2 deletions packages/calling/playwright/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,13 @@ export const verifySDKInitialized = async (page: Page): Promise<void> => {
timeout: SDK_INIT_TIMEOUT,
});

const hasCallingClient = await page.evaluate(() => !!(window as any).callingClient);
expect(hasCallingClient).toBe(true);
await expect
.poll(() => page.evaluate(() => !!(window as any).callingClient), {
timeout: SDK_INIT_TIMEOUT,
intervals: [500],
message: 'Expected window.callingClient to be available after SDK initialization',
})
.toBe(true);

// TODO: Based on the config passed during initialization, verify which clients are active.
// Different configs can instantiate CallingClient, CallHistoryClient, Voicemail, etc.
Expand Down
Loading
Loading