Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/samples/calling/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ <h2 class="collapsible">Authentication</h2>
<div id="guest-container" class="hidden">
<input id="jwt-token-for-dest" name="jwtToken" placeholder="JWT token for destination" value="" type="text" style="margin: 0.5rem 0 0.5rem 0;">
<input id="guest-name" name="guestName" placeholder="Guest name" value="" type="text" style="margin: 0.5rem 0 0.5rem 0;">
<button type="button" onclick="generateGuestToken()">Generate Guest Token [Prod only]</button>
<button id="generate-guest-token" type="button" onclick="generateGuestToken()">Generate Guest Token [Prod only]</button>
Comment thread
Shreyas281299 marked this conversation as resolved.
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"samples:serve": "webpack serve --color --env NODE_ENV=development",
"samples:serve:integration": "webpack serve --color --env NODE_ENV=test",
"samples:test": "NODE_ENV=test npx wdio run wdio.conf.js",
"test:e2e": "yarn workspaces foreach run test:e2e",
Comment thread
eigengravy marked this conversation as resolved.
"samples:test:mobile": "NODE_ENV=test wdio run ./wdio.conf.mobile.js",
"tooling": "node ./tooling/index.js",
"preinstall": "[ -f ./package-lock.json ] && [ \"$npm_config_refer\" != \"ci\" ] && npx npm-force-resolutions || exit 0",
Expand Down Expand Up @@ -107,6 +108,7 @@
"@babel/types": "^7.14.9",
"@commitlint/cli": "^17.1.2",
"@commitlint/config-conventional": "^17.1.0",
"@playwright/test": "^1.58.2",
Comment thread
eigengravy marked this conversation as resolved.
Comment thread
eigengravy marked this conversation as resolved.
Comment thread
Shreyas281299 marked this conversation as resolved.
"@sinonjs/fake-timers": "^6.0.1",
"@types/circular-dependency-plugin": "^5",
"@types/format-util": "^1",
Expand Down
3 changes: 3 additions & 0 deletions packages/calling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"build:docs": "typedoc --out ../../docs/calling",
"docs": "typedoc --emit none",
"test:e2e": "npx playwright test --config=playwright.config.ts",
"test:e2e:prod": "npx playwright test --config=playwright.config.ts --project='Calling SDK E2E - PROD'",
"test:e2e:int": "npx playwright test --config=playwright.config.ts --project='Calling SDK E2E - INT'",
"deploy:npm": "yarn npm publish"
},
"dependencies": {
Expand Down
100 changes: 100 additions & 0 deletions packages/calling/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {defineConfig, devices} from '@playwright/test';
import dotenv from 'dotenv';
import path from 'path';

// .env lives at repo root
dotenv.config({path: path.resolve(__dirname, '../../.env')});

const BASE_URL = process.env.PW_BASE_URL || 'https://localhost:8000';
Comment thread
eigengravy marked this conversation as resolved.

// Browser selection via PW_BROWSER env var: 'chrome' (default), 'firefox', 'edge', 'safari'
const PW_BROWSER = process.env.PW_BROWSER || 'chrome';

const chromiumArgs = [
'--disable-site-isolation-trials', // Allow cross-origin iframes in the same process
'--disable-web-security', // Bypass CORS for local dev server
'--no-sandbox', // Required for CI containers without root
'--disable-features=WebRtcHideLocalIpsWithMdns', // Expose real local IPs for WebRTC ICE candidates
'--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
'--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
...(process.env.CI ? [] : ['--auto-open-devtools-for-tabs']), // Open DevTools only in local runs
];

const browserOptions: Record<string, object> = {
chrome: {
...devices['Desktop Chrome'],
channel: 'chrome' as const,
launchOptions: {args: chromiumArgs},
},
edge: {
...devices['Desktop Edge'],
channel: 'msedge' as const,
launchOptions: {args: chromiumArgs},
},
firefox: {
...devices['Desktop Firefox'],
launchOptions: {
firefoxUserPrefs: {
'media.navigator.streams.fake': true, // Use fake media devices
'media.navigator.permission.disabled': true, // Auto-grant media permissions
},
},
},
safari: {
...devices['Desktop Safari'],
},
};

export default defineConfig({
testDir: './playwright',
timeout: 120000,
webServer: {
command: 'yarn samples:serve',
cwd: path.resolve(__dirname, '../..'),
url: BASE_URL,
ignoreHTTPSErrors: true,
reuseExistingServer: true,
stdout: 'ignore',
stderr: 'pipe',
},
retries: 3,
fullyParallel: false,
workers: 6,
reporter: 'html',
use: {
baseURL: BASE_URL,
ignoreHTTPSErrors: true,
trace: 'retain-on-failure',
},
projects: [
// Production
{
name: 'Calling: OAuth Setup - PROD',
testDir: './playwright/utils',
testMatch: /oauth\.setup\.ts/,
},
{
name: 'Calling SDK E2E - PROD',
dependencies: ['Calling: OAuth Setup - PROD'],
testDir: './playwright/tests',
use: browserOptions[PW_BROWSER],
},
// Integration
{
name: 'Calling: OAuth Setup - INT',
testDir: './playwright/utils',
testMatch: /oauth\.setup\.ts/,
use: {testEnv: 'int'} as any,
},
{
name: 'Calling SDK E2E - INT',
dependencies: ['Calling: OAuth Setup - INT'],
testDir: './playwright/tests',
use: {...browserOptions[PW_BROWSER], testEnv: 'int'} as any,
},
],
});
21 changes: 21 additions & 0 deletions packages/calling/playwright/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import path from 'path';

export type ServiceIndicator = 'calling' | 'contactcenter' | 'guestcalling';

// App paths
export const SAMPLE_APP_PATH = '/samples/calling/';
export const CC_SERVICE_DOMAIN = 'rtw.prod-us1.rtmsprod.net';

// Discovery
export const REGION = 'US-EAST';
export const COUNTRY = 'US';

// OAuth
export const ENV_PATH = path.resolve(__dirname, '../../../../.env');
export const DEVELOPER_PORTAL_GETTING_STARTED_URL =
'https://developer.webex.com/docs/getting-started';
export const DEVELOPER_PORTAL_INT_GETTING_STARTED_URL =
'https://developer-portal-intb.ciscospark.com/docs/getting-started';

export {CALLING_SELECTORS} from './selectors';
export {AWAIT_TIMEOUT, SDK_INIT_TIMEOUT, REGISTRATION_TIMEOUT, OPERATION_TIMEOUT} from './timeouts';
53 changes: 53 additions & 0 deletions packages/calling/playwright/constants/selectors.ts
Comment thread
Shreyas281299 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Element CALLING_SELECTORS (from the calling sample app)
export const CALLING_SELECTORS = {
// Authentication
ACCESS_TOKEN_INPUT: '#access-token',
INITIALIZE_CALLING_BTN: '#access-token-save',
AUTH_STATUS: '#access-token-status',
SERVICE_INDICATOR: '#ServiceIndicator',
SERVICE_DOMAIN: '#ServiceDomain',
REGION_INPUT: '#region',
COUNTRY_INPUT: '#country',
FEDRAMP_CHECKBOX: '#fedramp',
ENABLE_PRODUCTION_BTN: '#enableProduction',

// Registration
REGISTER_BTN: '#registration-register',
UNREGISTER_BTN: '#registration-unregister',
REGISTRATION_STATUS: '#registration-status',

// Call Controls
DESTINATION_INPUT: '#destination',
MAKE_CALL_BTN: '#create-call-action',
END_CALL_BTN: '#end-call',
ANSWER_BTN: '#answer',
MUTE_BTN: '#mute_button',
HOLD_BTN: '#hold_button',
DTMF_INPUT: '#dtmf_digit',
SEND_DIGIT_BTN: '#send-digit',

// Transfer
TRANSFER_TARGET_INPUT: '#transfer_target',
TRANSFER_OPTIONS: '#transfer-options',
TRANSFER_BTN: '#transfer',
END_SECOND_CALL_BTN: '#end-second',
TRANSFER_STATUS: '#transfer-call',

// Media
GET_MEDIA_STREAMS_BTN: '#sd-get-media-streams',
LOCAL_AUDIO: '#local-audio',
REMOTE_AUDIO: '#remote-audio',

// Guest Calling
GUEST_CONTAINER: '#guest-container',
JWT_TOKEN_FOR_DEST: '#jwt-token-for-dest',
GUEST_NAME: '#guest-name',
GENERATE_GUEST_TOKEN_BTN: '#generate-guest-token',

Comment thread
eigengravy marked this conversation as resolved.
// Call info
CALL_OBJECT: '#call-object',
INCOMING_CALL: '#incoming-call',
CALL_QUALITY_METRICS: '#call-quality-metrics',

END_BTN: '#end',
};
5 changes: 5 additions & 0 deletions packages/calling/playwright/constants/timeouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Timeouts — SDK timeout + 5s buffer for network/UI overhead
export const AWAIT_TIMEOUT = 10000; // General UI interactions
export const SDK_INIT_TIMEOUT = 65000; // RETRY_TIMER_UPPER_LIMIT (60s) + 5s
export const REGISTRATION_TIMEOUT = 35000; // BASE_REG_RETRY_TIMER_VAL_IN_SEC (30s) + 5s
export const OPERATION_TIMEOUT = 15000; // SUPPLEMENTARY_SERVICES_TIMEOUT (10s) + 5s
111 changes: 111 additions & 0 deletions packages/calling/playwright/tests/01-sdk-initialization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {test, expect} from '@playwright/test';
import {
navigateToCallingApp,
initializeCallingSDK,
verifySDKInitialized,
setServiceIndicator,
setServiceDomain,
setEnvironmentToInt,
setRegion,
setCountry,
waitForMobiusDiscoveryRequest,
verifyMobiusServersDiscovered,
} from '../utils/setup';
import {
CALLING_SELECTORS,
SDK_INIT_TIMEOUT,
AWAIT_TIMEOUT,
CC_SERVICE_DOMAIN,
REGION,
COUNTRY,
} from '../constants';

const getToken = (envVar: string): string => {
const token = process.env[envVar];
if (!token) {
throw new Error(`${envVar} not set. Run OAuth setup first.`);
}

return token;
};

test.describe('SDK Initialization', () => {
test.describe.configure({mode: 'parallel'});

test('Normal Calling - init with calling service indicator', async ({page}, testInfo) => {
const isInt = (testInfo.project.use as any).testEnv === 'int';
const envPrefix = isInt ? '_INT' : '';

await navigateToCallingApp(page);
if (isInt) await setEnvironmentToInt(page);
await setServiceIndicator(page, 'calling');

await initializeCallingSDK(page, getToken(`CALLER${envPrefix}_ACCESS_TOKEN`));
await verifySDKInitialized(page);
});

test('Contact Center - init with contactcenter service indicator', async ({page}, testInfo) => {
const isInt = (testInfo.project.use as any).testEnv === 'int';
const envPrefix = isInt ? '_INT' : '';

await navigateToCallingApp(page);
if (isInt) await setEnvironmentToInt(page);
await setServiceIndicator(page, 'contactcenter');
await setServiceDomain(page, CC_SERVICE_DOMAIN);

await initializeCallingSDK(page, getToken(`CALLER${envPrefix}_ACCESS_TOKEN`));
await verifySDKInitialized(page);
});

test('Guest Calling - generate guest token and init', async ({page}, testInfo) => {
const isInt = (testInfo.project.use as any).testEnv === 'int';
test.skip(isInt, 'Guest calling is prod-only');

await navigateToCallingApp(page);
await setServiceIndicator(page, 'guestcalling');

// Guest container should become visible after selecting guestcalling
await expect(page.locator(CALLING_SELECTORS.GUEST_CONTAINER)).toBeVisible({
timeout: AWAIT_TIMEOUT,
});

// Click "Generate Guest Token [Prod only]" - fetches JWT from AWS Lambda
await page.locator(CALLING_SELECTORS.GENERATE_GUEST_TOKEN_BTN).click({timeout: AWAIT_TIMEOUT});

// Wait for the token to be populated in the access token field
await expect(page.locator(CALLING_SELECTORS.ACCESS_TOKEN_INPUT)).not.toHaveValue('', {
timeout: SDK_INIT_TIMEOUT,
});

// Click "Initialize Calling" to init with the guest token
await page.locator(CALLING_SELECTORS.INITIALIZE_CALLING_BTN).click({timeout: AWAIT_TIMEOUT});
await verifySDKInitialized(page);
});

test('Normal Calling - init with explicit region and country', async ({page}, testInfo) => {
const isInt = (testInfo.project.use as any).testEnv === 'int';
const envPrefix = isInt ? '_INT' : '';

await navigateToCallingApp(page);
if (isInt) await setEnvironmentToInt(page);
await setServiceIndicator(page, 'calling');
await setCountry(page, COUNTRY);
await setRegion(page, REGION);

const mobiusDiscoveryRequest = waitForMobiusDiscoveryRequest(page, {
region: REGION,
country: COUNTRY,
});

await initializeCallingSDK(page, getToken(`CALLEE${envPrefix}_ACCESS_TOKEN`));
await verifySDKInitialized(page);

await expect(mobiusDiscoveryRequest).resolves.toContain(
`regionCode=${encodeURIComponent(REGION)}`
);
await expect(mobiusDiscoveryRequest).resolves.toContain(
`countryCode=${encodeURIComponent(COUNTRY)}`
);
await verifyMobiusServersDiscovered(page);
});
});
Loading
Loading