From a51b79c54042a00cb622491312ea3928fa28c88f Mon Sep 17 00:00:00 2001 From: Christina Mullen Date: Fri, 17 Jul 2026 07:41:43 -0500 Subject: [PATCH 1/2] feat(playwright): integrate campaign preview tests into e2e suite and add coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire campaign preview Playwright tests into the contact center e2e runner and add 9 new tests covering previously untested component behavior. Infrastructure: - constants.ts: add CAMPAIGN_TEST_IDS with all 21 data-testid values from the CampaignTask component tree (task, list item, title, phone, actions, buttons, expanded area, popover, countdown, global variables, error dialog) - test-manager.ts: add setupForCampaignPreview() — single agent, desktop login, console logging (campaign tests inject mock tasks via page.evaluate) - test-data.ts: add SET_10 with TEST_SUITE pointing to campaign-preview-tests.spec.ts so playwright.config.ts auto-discovers it Utility enhancement: - campaignPreviewUtils.ts: extend stubCampaignPreviewActions to support 'cancel' failAction (stubs task.end() to reject), add cancel tracking to getCampaignActionCounts return type New test coverage (9 tests across 6 describe blocks): - Cancel Error: cancel failure shows error dialog with correct title, dismiss re-enables cancel button - Button Disabled Combinations: both skip and remove disabled simultaneously, accept remains enabled - Countdown Display: countdown timer renders when timeout timestamp is set - Fallback Title: empty customer name falls back to ANI as title, phone subtitle hidden when it equals the title - Error Dialog Content: verifies correct error title text for accept, skip, and remove failures (3 tests) - Re-offer After Skip: simulated new contact (changed timeout) resets all buttons to enabled state Total campaign preview test count: 22 existing + 9 new = 31 tests --- playwright/Utils/campaignPreviewUtils.ts | 32 +- playwright/constants.ts | 24 ++ playwright/test-data.ts | 11 + playwright/test-manager.ts | 8 + .../tests/campaign-preview-test.spec.ts | 279 ++++++++++++++++++ 5 files changed, 345 insertions(+), 9 deletions(-) diff --git a/playwright/Utils/campaignPreviewUtils.ts b/playwright/Utils/campaignPreviewUtils.ts index 6a6c45e34..474f1dd30 100644 --- a/playwright/Utils/campaignPreviewUtils.ts +++ b/playwright/Utils/campaignPreviewUtils.ts @@ -298,18 +298,19 @@ export async function removeCampaignPreviewTask( /** * Stubs the SDK's campaign preview action methods (acceptPreviewContact, skipPreviewContact, - * removePreviewContact) on window['store'].cc so that test assertions can check whether - * the action was invoked and control whether it resolves or rejects. + * removePreviewContact) on window['store'].cc and the task's end() method (used by cancel) + * so that test assertions can check whether the action was invoked and control whether it + * resolves or rejects. * * @param page - Playwright Page object * @param failAction - If set, that action will reject with an error (for error dialog tests) */ export async function stubCampaignPreviewActions( page: Page, - failAction?: 'accept' | 'skip' | 'remove' + failAction?: 'accept' | 'skip' | 'remove' | 'cancel' ): Promise { await page.evaluate( - ({failAction}) => { + ({failAction, campaignInteractionId}) => { const store = (window as unknown as Record)['store'] as Record; if (!store) return; const cc = store.cc as Record unknown>; @@ -320,6 +321,7 @@ export async function stubCampaignPreviewActions( accept: 0, skip: 0, remove: 0, + cancel: 0, }); cc.acceptPreviewContact = () => { @@ -345,8 +347,20 @@ export async function stubCampaignPreviewActions( } return Promise.resolve({}); }; + + // Stub the task's end() method (used by cancelPreviewContact) if cancel failure is needed + if (failAction === 'cancel') { + const taskList = store.taskList as Record>; + const task = taskList[campaignInteractionId]; + if (task) { + task.end = () => { + tracker.cancel++; + return Promise.reject(new Error('Cancel failed (stubbed)')); + }; + } + } }, - {failAction: failAction ?? null} + {failAction: failAction ?? null, campaignInteractionId: CAMPAIGN_INTERACTION_ID} ); } @@ -354,16 +368,16 @@ export async function stubCampaignPreviewActions( * Returns the number of times each campaign action was called. * * @param page - Playwright Page object - * @returns Call counts for accept, skip, and remove + * @returns Call counts for accept, skip, remove, and cancel */ export async function getCampaignActionCounts( page: Page -): Promise<{accept: number; skip: number; remove: number}> { +): Promise<{accept: number; skip: number; remove: number; cancel: number}> { return page.evaluate(() => { const tracker = (window as unknown as Record)['__campaignPreviewCalls'] as - | {accept: number; skip: number; remove: number} + | {accept: number; skip: number; remove: number; cancel: number} | undefined; - return tracker ?? {accept: 0, skip: 0, remove: 0}; + return tracker ?? {accept: 0, skip: 0, remove: 0, cancel: 0}; }); } diff --git a/playwright/constants.ts b/playwright/constants.ts index 6f3fd8482..e82014549 100644 --- a/playwright/constants.ts +++ b/playwright/constants.ts @@ -112,6 +112,30 @@ export const RONA_OPTIONS = { export type RonaOption = (typeof RONA_OPTIONS)[keyof typeof RONA_OPTIONS]; +// Campaign Preview test IDs — must match data-testid values in CampaignTask components +export const CAMPAIGN_TEST_IDS = { + TASK: 'campaign-task', + LIST_ITEM: 'campaign-task-list-item', + TITLE: 'campaign-task-title', + PHONE: 'campaign-task-phone', + HANDLE_TIME: 'campaign-task-handle-time', + ACTIONS: 'campaign-task-actions', + ACCEPT_BUTTON: 'campaign-task-accept-button', + CONNECTING_BUTTON: 'campaign-task-connecting-button', + SKIP_REMOVE: 'campaign-task-skip-remove', + SKIP_BUTTON: 'campaign-task-skip-button', + REMOVE_BUTTON: 'campaign-task-remove-button', + EXPANDED: 'campaign-task-expanded', + CANCEL_BUTTON: 'campaign-task-cancel-button', + POPOVER: 'campaign-task-popover', + COUNTDOWN: 'campaign-countdown', + GLOBAL_VARIABLES_PANEL: 'global-variables-panel', + ERROR_DIALOG: 'campaign-error-dialog', + ERROR_DIALOG_TITLE: 'campaign-error-dialog-title', + ERROR_DIALOG_MESSAGE: 'campaign-error-dialog-message', + ERROR_DIALOG_OK: 'campaign-error-dialog-ok-button', +}; + // Test Data Constants export const TEST_DATA = { CHAT_NAME: 'Playwright Test', diff --git a/playwright/test-data.ts b/playwright/test-data.ts index 168d4b951..fc21f88b4 100644 --- a/playwright/test-data.ts +++ b/playwright/test-data.ts @@ -108,4 +108,15 @@ export const USER_SETS = { ENTRY_POINT: env.PW_ENTRY_POINT9, TEST_SUITE: 'multiparty-conference-set-9-tests.spec.ts', }, + SET_10: { + AGENTS: { + AGENT1: {username: 'user37', extension: '1037', agentName: 'User37 Agent37'}, + AGENT2: {username: 'user38', extension: '1038', agentName: 'User38 Agent38'}, + }, + QUEUE_NAME: 'Queue e2e 10', + CHAT_URL: `${env.PW_CHAT_URL}-e2e-10.html`, + EMAIL_ENTRY_POINT: `${env.PW_SANDBOX}.e2e10@gmail.com`, + ENTRY_POINT: env.PW_ENTRY_POINT10, + TEST_SUITE: 'campaign-preview-tests.spec.ts', + }, }; diff --git a/playwright/test-manager.ts b/playwright/test-manager.ts index d5d8318ab..0c6ad3a0d 100644 --- a/playwright/test-manager.ts +++ b/playwright/test-manager.ts @@ -552,6 +552,14 @@ export class TestManager { ); } + async setupForCampaignPreview(browser: Browser): Promise { + await this.setup(browser, { + needsAgent1: true, + agent1LoginMode: LOGIN_MODE.DESKTOP, + enableConsoleLogging: true, + }); + } + async setupForMultipartyConference(browser: Browser) { await this.setup(browser, { needsAgent1: true, diff --git a/playwright/tests/campaign-preview-test.spec.ts b/playwright/tests/campaign-preview-test.spec.ts index d51fc59b2..2527644a4 100644 --- a/playwright/tests/campaign-preview-test.spec.ts +++ b/playwright/tests/campaign-preview-test.spec.ts @@ -525,4 +525,283 @@ export default function createCampaignPreviewTests() { await waitForCampaignTaskHidden(testManager.agent1Page); }); }); + + test.describe('Campaign Preview - Cancel Error', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should show error dialog when cancel fails', async () => { + await injectCampaignPreviewTask(testManager.agent1Page); + await waitForCampaignTaskVisible(testManager.agent1Page); + + // Stub actions with cancel failure AFTER task injection so the task's end() gets overwritten + await stubCampaignPreviewActions(testManager.agent1Page, 'cancel'); + + await clickCampaignCancel(testManager.agent1Page); + + // Error dialog should appear with the cancel-specific title + const errorDialog = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG).first(); + await expect(errorDialog).toBeVisible({timeout: 5000}); + + const errorTitle = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_TITLE).first(); + await expect(errorTitle).toContainText("Can't cancel contact"); + + // Dismiss the error dialog + const okButton = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_OK).first(); + await okButton.click(); + await expect(errorDialog).toBeHidden({timeout: 5000}); + + // Cancel button should be re-enabled after error dismissal + const cancelBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CANCEL_BUTTON).first(); + await expect(cancelBtn).toBeVisible(); + await expect(cancelBtn).toBeEnabled(); + }); + }); + + test.describe('Campaign Preview - Button Disabled Combinations', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should render both skip and remove buttons disabled when both are configured', async () => { + await stubCampaignPreviewActions(testManager.agent1Page); + await injectCampaignPreviewTask(testManager.agent1Page, { + skipDisabled: 'true', + removeDisabled: 'true', + }); + await waitForCampaignTaskVisible(testManager.agent1Page); + + const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); + const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); + await expect(skipBtn).toBeDisabled(); + await expect(removeBtn).toBeDisabled(); + + // Accept should still be enabled — it's the only action available + const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); + await expect(acceptBtn).toBeEnabled(); + }); + }); + + test.describe('Campaign Preview - Countdown Display', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should display countdown timer before timeout', async () => { + await stubCampaignPreviewActions(testManager.agent1Page); + // Use a generous timeout so countdown is visible during assertion + await injectCampaignPreviewTask(testManager.agent1Page, { + offerTimeout: String(Date.now() + 60000), + }); + await waitForCampaignTaskVisible(testManager.agent1Page); + + // The countdown element should be visible in the popover's list item + // The popover uses testIdPrefix="campaign-popover" and shows the countdown + // in the main list item the countdown is only shown when timerDisplayMode=auto (popover) + // The inline card uses timerDisplayMode=handle-time, so countdown only appears in popover. + // Check the campaign-countdown element directly + const countdown = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.COUNTDOWN).first(); + await expect(countdown).toBeAttached({timeout: 5000}); + }); + }); + + test.describe('Campaign Preview - Fallback Title', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should use phone number as title when customer name is not provided', async () => { + await stubCampaignPreviewActions(testManager.agent1Page); + await injectCampaignPreviewTask(testManager.agent1Page, { + customerName: '', + ani: '+14085551234', + }); + await waitForCampaignTaskVisible(testManager.agent1Page); + + // Title should fall back to ANI since customerName is empty + const title = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.TITLE).first(); + await expect(title).toContainText('+14085551234'); + + // Phone subtitle should not be visible when it equals the title + const phone = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.PHONE).first(); + await expect(phone).toBeHidden(); + }); + }); + + test.describe('Campaign Preview - Error Dialog Content', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should display correct error title for accept failure', async () => { + await stubCampaignPreviewActions(testManager.agent1Page, 'accept'); + await injectCampaignPreviewTask(testManager.agent1Page); + await waitForCampaignTaskVisible(testManager.agent1Page); + + await clickCampaignAccept(testManager.agent1Page); + + const errorTitle = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_TITLE).first(); + await expect(errorTitle).toContainText("Can't accept contact"); + + const errorMessage = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_MESSAGE).first(); + await expect(errorMessage).toBeVisible(); + + // Dismiss + await testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_OK).first().click(); + }); + + test('should display correct error title for skip failure', async () => { + await stubCampaignPreviewActions(testManager.agent1Page, 'skip'); + await injectCampaignPreviewTask(testManager.agent1Page); + await waitForCampaignTaskVisible(testManager.agent1Page); + + await clickCampaignSkip(testManager.agent1Page); + + const errorTitle = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_TITLE).first(); + await expect(errorTitle).toContainText("Can't skip contact"); + + // Dismiss + await testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_OK).first().click(); + }); + + test('should display correct error title for remove failure', async () => { + await stubCampaignPreviewActions(testManager.agent1Page, 'remove'); + await injectCampaignPreviewTask(testManager.agent1Page); + await waitForCampaignTaskVisible(testManager.agent1Page); + + await clickCampaignRemove(testManager.agent1Page); + + const errorTitle = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_TITLE).first(); + await expect(errorTitle).toContainText("Can't remove contact"); + + // Dismiss + await testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ERROR_DIALOG_OK).first().click(); + }); + }); + + test.describe('Campaign Preview - Re-offer After Skip', () => { + let testManager: TestManager; + + test.beforeAll(async ({browser}, testInfo) => { + const projectName = testInfo.project.name; + testManager = new TestManager(projectName); + await testManager.setupForCampaignPreview(browser); + }); + + test.afterAll(async () => { + await testManager.cleanup(); + }); + + test.afterEach(async () => { + await removeCampaignPreviewTask(testManager.agent1Page).catch(() => {}); + }); + + test('should reset buttons when a new contact is offered after skip', async () => { + await stubCampaignPreviewActions(testManager.agent1Page); + await injectCampaignPreviewTask(testManager.agent1Page, { + offerTimeout: String(Date.now() + 60000), + }); + await waitForCampaignTaskVisible(testManager.agent1Page); + + // Skip the first contact + await clickCampaignSkip(testManager.agent1Page); + + // Verify buttons are disabled after skip + const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); + await expect(acceptBtn).toBeDisabled(); + + // Simulate a new contact offer by updating the task's timeout timestamp + // (the component resets state when timeoutTimestamp changes and task is not accepted) + await testManager.agent1Page.evaluate( + ({interactionId}) => { + const store = (window as unknown as Record)['store'] as Record; + if (!store) return; + const taskList = store.taskList as Record>; + const task = taskList[interactionId]; + if (!task) return; + + const data = task.data as Record; + const interaction = data.interaction as Record; + const cpd = interaction.callProcessingDetails as Record; + + // Change the timeout to simulate a new contact offer + cpd.campaignPreviewOfferTimeout = String(Date.now() + 60000); + + // Trigger MobX reactivity + store.taskList = {...taskList}; + }, + {interactionId: 'campaign-preview-e2e-001'} + ); + + // After re-offer, accept button should become enabled again + await expect(acceptBtn).toBeEnabled({timeout: 5000}); + + // Skip and remove should also be re-enabled (based on their config, not disabled) + const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); + const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); + await expect(skipBtn).toBeEnabled({timeout: 5000}); + await expect(removeBtn).toBeEnabled({timeout: 5000}); + }); + }); } From b6435cc252982d8512551820f3398fec28c026fe Mon Sep 17 00:00:00 2001 From: Christina Mullen Date: Wed, 22 Jul 2026 15:11:22 -0500 Subject: [PATCH 2/2] fix tests --- playwright/Utils/campaignPreviewUtils.ts | 92 ++++++++++++++----- playwright/test-data.ts | 4 +- .../tests/campaign-preview-test.spec.ts | 84 ++++++++++++----- 3 files changed, 134 insertions(+), 46 deletions(-) diff --git a/playwright/Utils/campaignPreviewUtils.ts b/playwright/Utils/campaignPreviewUtils.ts index 474f1dd30..a154a3ff0 100644 --- a/playwright/Utils/campaignPreviewUtils.ts +++ b/playwright/Utils/campaignPreviewUtils.ts @@ -98,10 +98,13 @@ export async function injectCampaignPreviewTask( interactionState, globalVariables, }) => { - const store = (window as unknown as Record)['store'] as Record; - if (!store) { + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) { throw new Error('Store not found on window object'); } + // Access the inner MobX store — the wrapper's taskList is a getter-only + // property, so assignments to it silently fail without triggering observers. + const store = storeWrapper.store as Record; // Build callAssociatedData from globalVariables const callAssociatedData: Record< @@ -134,7 +137,7 @@ export async function injectCampaignPreviewTask( }; } - const agentId = store.agentId as string; + const agentId = (storeWrapper.agentId ?? store.agentId) as string; // Build a minimal mock task matching the ITask shape. // The EventEmitter methods are stubs since E2E doesn't fire real SDK events. @@ -239,14 +242,14 @@ export async function injectCampaignPreviewTask( toggleMute: noOp, }; - // Inject the task into the store's task list. - // Use MobX runInAction-like direct assignment — the store is already - // wrapped with makeAutoObservable so direct mutations are tracked. - const taskList = store.taskList as Record; - taskList[interactionId] = mockTask; - - // Trigger MobX reactivity by reassigning taskList - store.taskList = {...taskList}; + // Inject the task into the inner store's taskList. + // Assigning a new object to the MobX-observable property triggers + // observer notifications and React re-renders. + const existingTaskList = store.taskList as Record | undefined; + store.taskList = { + ...(existingTaskList ?? {}), + [interactionId]: mockTask, + }; }, { interactionId, @@ -278,11 +281,14 @@ export async function removeCampaignPreviewTask( ): Promise { await page.evaluate( ({interactionId}) => { - const store = (window as unknown as Record)['store'] as Record; - if (!store) return; + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + const store = storeWrapper.store as Record; + const taskList = store.taskList as Record; - delete taskList[interactionId]; - store.taskList = {...taskList}; + const updated = {...taskList}; + delete updated[interactionId]; + store.taskList = updated; // Also clean up acceptedCampaignIds const acceptedIds = store.acceptedCampaignIds as Set; @@ -296,6 +302,46 @@ export async function removeCampaignPreviewTask( ); } +/** + * Stubs the StoreWrapper's refreshTaskList method to prevent real SDK events + * (heartbeats, AGENT_CONTACT, etc.) from overwriting mock-injected tasks. + * + * In the E2E environment, the agent is logged into a real sandbox. + * Any SDK event that fires will call refreshTaskList(), which replaces + * store.taskList with cc.taskManager.getAllTasks() — an empty map since + * our tasks are mock-injected and not known to the real SDK task manager. + * + * Call this BEFORE injecting mock tasks to ensure they persist. + * + * @param page - Playwright Page object + */ +export async function stubRefreshTaskList(page: Page): Promise { + await page.evaluate(() => { + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + // Replace refreshTaskList with a no-op to prevent SDK events from clearing mock tasks + (storeWrapper as Record void>).refreshTaskList = () => {}; + }); +} + +/** + * Restores the StoreWrapper's original refreshTaskList method. + * Call this during test cleanup to avoid affecting other tests. + * + * @param page - Playwright Page object + */ +export async function restoreRefreshTaskList(page: Page): Promise { + await page.evaluate(() => { + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + // Restore the original method from the prototype + const proto = Object.getPrototypeOf(storeWrapper) as Record; + if (proto && typeof proto.refreshTaskList === 'function') { + (storeWrapper as Record).refreshTaskList = proto.refreshTaskList; + } + }); +} + /** * Stubs the SDK's campaign preview action methods (acceptPreviewContact, skipPreviewContact, * removePreviewContact) on window['store'].cc and the task's end() method (used by cancel) @@ -311,8 +357,9 @@ export async function stubCampaignPreviewActions( ): Promise { await page.evaluate( ({failAction, campaignInteractionId}) => { - const store = (window as unknown as Record)['store'] as Record; - if (!store) return; + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + const store = storeWrapper.store as Record; const cc = store.cc as Record unknown>; if (!cc) return; @@ -350,8 +397,8 @@ export async function stubCampaignPreviewActions( // Stub the task's end() method (used by cancelPreviewContact) if cancel failure is needed if (failAction === 'cancel') { - const taskList = store.taskList as Record>; - const task = taskList[campaignInteractionId]; + const innerTaskList = store.taskList as Record>; + const task = innerTaskList[campaignInteractionId]; if (task) { task.end = () => { tracker.cancel++; @@ -482,8 +529,9 @@ export async function expireCampaignTimeout( ): Promise { await page.evaluate( ({interactionId}) => { - const store = (window as unknown as Record)['store'] as Record; - if (!store) return; + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + const store = storeWrapper.store as Record; const taskList = store.taskList as Record>; const task = taskList[interactionId] as Record | undefined; if (!task) return; @@ -495,7 +543,7 @@ export async function expireCampaignTimeout( // Set timeout to 1 second from now so countdown expires quickly cpd.campaignPreviewOfferTimeout = String(Date.now() + 1000); - // Trigger MobX reactivity + // Trigger MobX reactivity by reassigning taskList on inner store store.taskList = {...taskList}; }, {interactionId} diff --git a/playwright/test-data.ts b/playwright/test-data.ts index fc21f88b4..0ecf4d340 100644 --- a/playwright/test-data.ts +++ b/playwright/test-data.ts @@ -110,8 +110,8 @@ export const USER_SETS = { }, SET_10: { AGENTS: { - AGENT1: {username: 'user37', extension: '1037', agentName: 'User37 Agent37'}, - AGENT2: {username: 'user38', extension: '1038', agentName: 'User38 Agent38'}, + AGENT1: {username: 'user1', extension: '1001', agentName: 'User1 Agent1'}, + AGENT2: {username: 'user1', extension: '1001', agentName: 'User1 Agent1'}, }, QUEUE_NAME: 'Queue e2e 10', CHAT_URL: `${env.PW_CHAT_URL}-e2e-10.html`, diff --git a/playwright/tests/campaign-preview-test.spec.ts b/playwright/tests/campaign-preview-test.spec.ts index 2527644a4..238db1ee0 100644 --- a/playwright/tests/campaign-preview-test.spec.ts +++ b/playwright/tests/campaign-preview-test.spec.ts @@ -5,6 +5,8 @@ import { injectCampaignPreviewTask, removeCampaignPreviewTask, stubCampaignPreviewActions, + stubRefreshTaskList, + restoreRefreshTaskList, getCampaignActionCounts, waitForCampaignTaskVisible, waitForCampaignTaskHidden, @@ -23,9 +25,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -37,6 +41,7 @@ export default function createCampaignPreviewTests() { test('should render campaign preview task when injected into task list', async () => { await stubCampaignPreviewActions(testManager.agent1Page); await injectCampaignPreviewTask(testManager.agent1Page); + await waitForCampaignTaskVisible(testManager.agent1Page); // Verify core UI elements are rendered @@ -69,10 +74,12 @@ export default function createCampaignPreviewTests() { }); await waitForCampaignTaskVisible(testManager.agent1Page); + // The expanded area is below the inline list item row const expandedArea = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.EXPANDED).first(); await expect(expandedArea).toBeVisible(); - const variablesPanel = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.GLOBAL_VARIABLES_PANEL).first(); + // Scope to the expanded area to avoid matching the hidden popover's panel + const variablesPanel = expandedArea.getByTestId(CAMPAIGN_TEST_IDS.GLOBAL_VARIABLES_PANEL); await expect(variablesPanel).toBeVisible(); }); @@ -103,9 +110,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -181,9 +190,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -198,11 +209,13 @@ export default function createCampaignPreviewTests() { await clickCampaignSkip(testManager.agent1Page); - // All buttons should be disabled after skip - const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); + // After skip, the accept button is replaced by a status button showing "Skipping..." + const connectingBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CONNECTING_BUTTON).first(); + await expect(connectingBtn).toBeVisible(); + await expect(connectingBtn).toBeDisabled(); + const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); - await expect(acceptBtn).toBeDisabled(); await expect(skipBtn).toBeDisabled(); await expect(removeBtn).toBeDisabled(); @@ -251,9 +264,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -268,11 +283,13 @@ export default function createCampaignPreviewTests() { await clickCampaignRemove(testManager.agent1Page); - // All buttons should be disabled after remove - const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); + // After remove, the accept button is replaced by a status button showing "Removing..." + const connectingBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CONNECTING_BUTTON).first(); + await expect(connectingBtn).toBeVisible(); + await expect(connectingBtn).toBeDisabled(); + const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); - await expect(acceptBtn).toBeDisabled(); await expect(skipBtn).toBeDisabled(); await expect(removeBtn).toBeDisabled(); @@ -321,9 +338,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -359,9 +378,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -399,10 +420,10 @@ export default function createCampaignPreviewTests() { }); await waitForCampaignTaskVisible(testManager.agent1Page); - // Wait for timeout — all buttons should become disabled - await expect( - testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first() - ).toBeDisabled({timeout: 10000}); + // After timeout with SKIP auto-action, accept button is replaced by status button + const connectingBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CONNECTING_BUTTON).first(); + await expect(connectingBtn).toBeVisible({timeout: 10000}); + await expect(connectingBtn).toBeDisabled(); const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); @@ -418,10 +439,10 @@ export default function createCampaignPreviewTests() { }); await waitForCampaignTaskVisible(testManager.agent1Page); - // Wait for timeout — all buttons should become disabled - await expect( - testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first() - ).toBeDisabled({timeout: 10000}); + // After timeout with REMOVE auto-action, accept button is replaced by status button + const connectingBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CONNECTING_BUTTON).first(); + await expect(connectingBtn).toBeVisible({timeout: 10000}); + await expect(connectingBtn).toBeDisabled(); const skipBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.SKIP_BUTTON).first(); const removeBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.REMOVE_BUTTON).first(); @@ -437,9 +458,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -503,9 +526,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -533,9 +558,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -578,9 +605,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -614,9 +643,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -649,9 +680,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -684,9 +717,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -747,9 +782,11 @@ export default function createCampaignPreviewTests() { const projectName = testInfo.project.name; testManager = new TestManager(projectName); await testManager.setupForCampaignPreview(browser); + await stubRefreshTaskList(testManager.agent1Page); }); test.afterAll(async () => { + await restoreRefreshTaskList(testManager.agent1Page).catch(() => {}); await testManager.cleanup(); }); @@ -767,16 +804,18 @@ export default function createCampaignPreviewTests() { // Skip the first contact await clickCampaignSkip(testManager.agent1Page); - // Verify buttons are disabled after skip - const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); - await expect(acceptBtn).toBeDisabled(); + // After skip, accept button is replaced by status button + const connectingBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.CONNECTING_BUTTON).first(); + await expect(connectingBtn).toBeVisible(); + await expect(connectingBtn).toBeDisabled(); // Simulate a new contact offer by updating the task's timeout timestamp // (the component resets state when timeoutTimestamp changes and task is not accepted) await testManager.agent1Page.evaluate( ({interactionId}) => { - const store = (window as unknown as Record)['store'] as Record; - if (!store) return; + const storeWrapper = (window as unknown as Record)['store'] as Record; + if (!storeWrapper) return; + const store = storeWrapper.store as Record; const taskList = store.taskList as Record>; const task = taskList[interactionId]; if (!task) return; @@ -788,13 +827,14 @@ export default function createCampaignPreviewTests() { // Change the timeout to simulate a new contact offer cpd.campaignPreviewOfferTimeout = String(Date.now() + 60000); - // Trigger MobX reactivity + // Trigger MobX reactivity by reassigning taskList on inner store store.taskList = {...taskList}; }, {interactionId: 'campaign-preview-e2e-001'} ); - // After re-offer, accept button should become enabled again + // After re-offer, accept button should reappear and become enabled again + const acceptBtn = testManager.agent1Page.getByTestId(CAMPAIGN_TEST_IDS.ACCEPT_BUTTON).first(); await expect(acceptBtn).toBeEnabled({timeout: 5000}); // Skip and remove should also be re-enabled (based on their config, not disabled)