Implementing Playwright End-to-End Test Suites for Web Messaging Customer Journeys
What This Guide Covers
You are building a Playwright-based end-to-end testing framework that simulates complete customer journeys through the Genesys Cloud Web Messaging widget - from the moment a customer opens the messenger on your website, through the conversation with an agent (or bot), to the final survey submission and conversation close. When complete, your CI/CD pipeline will run automated tests that validate: widget loads correctly on your site, customer can send a message, the message is routed to the correct Genesys Cloud queue, an agent (simulated) can respond, file attachments work, quick reply buttons are functional, the conversation can be disconnected, and the post-conversation survey renders. This catches integration failures between your website, the Messenger SDK, and Genesys Cloud routing before customers encounter them.
Prerequisites, Roles & Licensing
- Genesys Cloud: CX 2 or 3 with Web Messaging enabled.
- Tooling:
- Playwright (latest stable)
- A staging environment with the Genesys Cloud Messenger widget deployed
- A test Genesys Cloud organization with a dedicated test queue and test agent credentials
- API access:
- OAuth Client Credentials for programmatic agent operations via the Platform API
The Implementation Deep-Dive
1. Test Architecture
[Playwright Test Runner]
|
├── [Customer Browser Context] ── Opens your website with Messenger widget
| |
| └── Sends messages, clicks quick replies, uploads files
|
├── [Agent API Context] ── Uses Genesys Cloud Platform API to simulate agent
| |
| └── Accepts interactions, sends replies, disconnects
|
└── [Assertions] ── Validates routing, message delivery, UI states
2. Test Fixtures and Helper Classes
// tests/e2e/fixtures.ts
import { test as base, Page } from '@playwright/test';
import axios from 'axios';
const GENESYS_API = 'https://api.mypurecloud.com';
const GENESYS_LOGIN = 'https://login.mypurecloud.com';
interface GenesysAgentContext {
token: string;
userId: string;
setPresence: (presenceId: string) => Promise<void>;
acceptInteraction: (conversationId: string) => Promise<void>;
sendAgentMessage: (conversationId: string, text: string) => Promise<void>;
disconnectInteraction: (conversationId: string) => Promise<void>;
getConversations: () => Promise<any[]>;
}
async function createAgentContext(): Promise<GenesysAgentContext> {
// Authenticate with OAuth client credentials
const authResp = await axios.post(`${GENESYS_LOGIN}/oauth/token`,
'grant_type=client_credentials', {
auth: {
username: process.env.GC_TEST_CLIENT_ID!,
password: process.env.GC_TEST_CLIENT_SECRET!
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
);
const token = authResp.data.access_token;
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
// Get test agent user ID
const userResp = await axios.get(`${GENESYS_API}/api/v2/users/me`, { headers });
const userId = userResp.data.id;
return {
token,
userId,
async setPresence(presenceDefinitionId: string) {
await axios.patch(
`${GENESYS_API}/api/v2/users/${userId}/presences/PURECLOUD`,
{ presenceDefinition: { id: presenceDefinitionId } },
{ headers }
);
},
async acceptInteraction(conversationId: string) {
// Get the participant ID for the agent
const conv = await axios.get(
`${GENESYS_API}/api/v2/conversations/messages/${conversationId}`,
{ headers }
);
const agentParticipant = conv.data.participants.find(
(p: any) => p.purpose === 'agent' && p.state === 'alerting'
);
if (agentParticipant) {
await axios.patch(
`${GENESYS_API}/api/v2/conversations/messages/${conversationId}/participants/${agentParticipant.id}`,
{ state: 'connected' },
{ headers }
);
}
},
async sendAgentMessage(conversationId: string, text: string) {
await axios.post(
`${GENESYS_API}/api/v2/conversations/messages/${conversationId}/communications`,
{
textBody: text,
bodyType: 'standard'
},
{ headers }
);
},
async disconnectInteraction(conversationId: string) {
const conv = await axios.get(
`${GENESYS_API}/api/v2/conversations/messages/${conversationId}`,
{ headers }
);
const agentParticipant = conv.data.participants.find(
(p: any) => p.purpose === 'agent' && p.state === 'connected'
);
if (agentParticipant) {
await axios.patch(
`${GENESYS_API}/api/v2/conversations/messages/${conversationId}/participants/${agentParticipant.id}`,
{ state: 'disconnected' },
{ headers }
);
}
},
async getConversations() {
const resp = await axios.get(
`${GENESYS_API}/api/v2/conversations`,
{ headers }
);
return resp.data.entities || [];
}
};
}
// Export as Playwright fixture
export const test = base.extend<{ agent: GenesysAgentContext }>({
agent: async ({}, use) => {
const agent = await createAgentContext();
// Set agent to "On Queue" status
await agent.setPresence(process.env.GC_ON_QUEUE_PRESENCE_ID!);
await use(agent);
}
});
export { expect } from '@playwright/test';
3. Customer Journey Test: Full Conversation
// tests/e2e/web-messaging-journey.test.ts
import { test, expect } from './fixtures';
test.describe('Web Messaging Customer Journey', () => {
test('complete conversation from open to close', async ({ page, agent }) => {
// 1. Customer opens website with Messenger widget
await page.goto(process.env.TEST_WEBSITE_URL!);
// 2. Click the Messenger launcher button
const launcher = page.frameLocator('#genesys-messenger-frame')
.locator('[data-testid="messenger-launcher-button"]');
await launcher.click();
// 3. Wait for messenger window to open
const messengerFrame = page.frameLocator('#genesys-messenger-frame');
await expect(messengerFrame.locator('[data-testid="messenger-header"]')).toBeVisible();
// 4. Customer types and sends a message
const input = messengerFrame.locator('[data-testid="messenger-input-field"]');
await input.fill('I need help with my account billing');
await input.press('Enter');
// 5. Verify the customer message appears in the chat
await expect(
messengerFrame.locator('[data-testid="message-bubble"]').last()
).toContainText('I need help with my account billing');
// 6. Wait for interaction to route to agent (poll Genesys API)
let conversationId: string | null = null;
for (let attempt = 0; attempt < 30; attempt++) {
const conversations = await agent.getConversations();
const webMsg = conversations.find(
(c: any) => c.participants?.some(
(p: any) => p.purpose === 'agent' && p.state === 'alerting'
)
);
if (webMsg) {
conversationId = webMsg.id;
break;
}
await page.waitForTimeout(2000);
}
expect(conversationId).not.toBeNull();
// 7. Agent accepts the interaction
await agent.acceptInteraction(conversationId!);
// 8. Agent sends a reply
await agent.sendAgentMessage(conversationId!, 'Hello! I can help with your billing question. What specifically do you need?');
// 9. Verify agent message appears in customer's Messenger
await expect(
messengerFrame.locator('[data-testid="message-bubble"]').last()
).toContainText('Hello! I can help with your billing question', { timeout: 15000 });
// 10. Customer responds
await input.fill('I was charged twice for my subscription');
await input.press('Enter');
// 11. Agent resolves and disconnects
await agent.sendAgentMessage(conversationId!, 'I see the duplicate charge. I have initiated a refund. Is there anything else?');
await page.waitForTimeout(3000);
await agent.disconnectInteraction(conversationId!);
// 12. Verify the "conversation ended" message appears in Messenger
await expect(
messengerFrame.locator('[data-testid="conversation-ended"]')
).toBeVisible({ timeout: 10000 });
});
test('messenger widget loads and renders correctly', async ({ page }) => {
await page.goto(process.env.TEST_WEBSITE_URL!);
// Verify launcher button is present
const frame = page.frameLocator('#genesys-messenger-frame');
await expect(frame.locator('[data-testid="messenger-launcher-button"]')).toBeVisible();
// Click to open
await frame.locator('[data-testid="messenger-launcher-button"]').click();
// Verify header, input, and send button are present
await expect(frame.locator('[data-testid="messenger-header"]')).toBeVisible();
await expect(frame.locator('[data-testid="messenger-input-field"]')).toBeVisible();
});
});
4. CI/CD Pipeline Configuration
# .github/workflows/e2e-messaging.yml
name: E2E Web Messaging Tests
on:
schedule:
- cron: '0 6 * * 1-5' # Mon-Fri at 6 AM UTC (before business hours)
workflow_dispatch:
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
environment: staging
env:
GC_TEST_CLIENT_ID: ${{ secrets.GC_TEST_CLIENT_ID }}
GC_TEST_CLIENT_SECRET: ${{ secrets.GC_TEST_CLIENT_SECRET }}
GC_ON_QUEUE_PRESENCE_ID: ${{ secrets.GC_ON_QUEUE_PRESENCE_ID }}
TEST_WEBSITE_URL: ${{ vars.TEST_WEBSITE_URL }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Run E2E Tests
run: npx playwright test tests/e2e/ --reporter=html
- name: Upload Report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
- name: Notify on Failure
if: failure()
run: |
curl -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H 'Content-type: application/json' \
-d '{"text":"🔴 Web Messaging E2E tests FAILED. Check: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}'
Validation, Edge Cases & Troubleshooting
Edge Case 1: No Agent Available - Conversation Sits in Queue
The test sends a customer message but no agent is on-queue in the test org. The test times out waiting for an interaction to alert.
Solution: The test fixture explicitly sets the test agent to “On Queue” presence before each test. If the presence API call fails (e.g., the test agent’s status is “On Break” from a previous failed test), catch the error and retry with a fresh presence update. Add a test teardown that always resets the agent to “Off Queue.”
Edge Case 2: Messenger iFrame Selectors Change After Genesys SDK Update
Genesys periodically updates the Messenger widget’s internal DOM structure. Your [data-testid] selectors break silently.
Solution: Use the most stable selectors available - data-testid attributes are more stable than class names. Subscribe to Genesys Cloud release notes and pin the Messenger SDK version in your deployment snippet (version: "2" → version: "2.1.0"). Maintain a selector map file that’s easy to update when the SDK changes.
Edge Case 3: Race Condition Between Customer Message Send and Agent Routing
The customer sends a message, but the test’s API polling for the agent’s alerting conversation starts before Genesys has finished routing. The test finds no conversations and fails.
Solution: Use exponential backoff in the polling loop (2s, 4s, 6s…) with a maximum of 30 attempts (60 seconds total). Web Messaging routing typically completes within 3-5 seconds. If no conversation appears after 60 seconds, the failure is real (routing misconfiguration, queue not matching, agent not in queue).