Implementing Playwright End-to-End Test Suites for Web Messaging Customer Journeys

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).

Official References