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
120 changes: 91 additions & 29 deletions playwright/Utils/campaignPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ export async function injectCampaignPreviewTask(
interactionState,
globalVariables,
}) => {
const store = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!store) {
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
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<string, unknown>;

// Build callAssociatedData from globalVariables
const callAssociatedData: Record<
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<string, unknown>;
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<string, unknown> | undefined;
store.taskList = {
...(existingTaskList ?? {}),
[interactionId]: mockTask,
};
},
{
interactionId,
Expand Down Expand Up @@ -278,11 +281,14 @@ export async function removeCampaignPreviewTask(
): Promise<void> {
await page.evaluate(
({interactionId}) => {
const store = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!store) return;
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!storeWrapper) return;
const store = storeWrapper.store as Record<string, unknown>;

const taskList = store.taskList as Record<string, unknown>;
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<string>;
Expand All @@ -296,22 +302,64 @@ 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<void> {
await page.evaluate(() => {
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!storeWrapper) return;
// Replace refreshTaskList with a no-op to prevent SDK events from clearing mock tasks
(storeWrapper as Record<string, () => 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<void> {
await page.evaluate(() => {
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!storeWrapper) return;
// Restore the original method from the prototype
const proto = Object.getPrototypeOf(storeWrapper) as Record<string, unknown>;
if (proto && typeof proto.refreshTaskList === 'function') {
(storeWrapper as Record<string, unknown>).refreshTaskList = proto.refreshTaskList;
Comment on lines +338 to +340

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the saved refreshTaskList function

When restoreRefreshTaskList() runs, this prototype lookup never finds the original method: I checked StoreWrapper and refreshTaskList is declared as an instance field (refreshTaskList = ...), so it lives on the store wrapper instance rather than Object.getPrototypeOf(storeWrapper). After any test stubs refreshes in a page/context, the helper leaves refreshTaskList as a no-op, so cleanup or later checks in that context cannot observe real SDK task updates; save the original function before overwriting it and restore that saved reference instead.

Useful? React with 👍 / 👎.

}
});
}

/**
* 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<void> {
await page.evaluate(
({failAction}) => {
const store = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!store) return;
({failAction, campaignInteractionId}) => {
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!storeWrapper) return;
const store = storeWrapper.store as Record<string, unknown>;
const cc = store.cc as Record<string, (...args: unknown[]) => unknown>;
if (!cc) return;

Expand All @@ -320,6 +368,7 @@ export async function stubCampaignPreviewActions(
accept: 0,
skip: 0,
remove: 0,
cancel: 0,
});

cc.acceptPreviewContact = () => {
Expand All @@ -345,25 +394,37 @@ 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 innerTaskList = store.taskList as Record<string, Record<string, unknown>>;
const task = innerTaskList[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}
);
}

/**
* 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<string, unknown>)['__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};
});
}

Expand Down Expand Up @@ -468,8 +529,9 @@ export async function expireCampaignTimeout(
): Promise<void> {
await page.evaluate(
({interactionId}) => {
const store = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!store) return;
const storeWrapper = (window as unknown as Record<string, unknown>)['store'] as Record<string, unknown>;
if (!storeWrapper) return;
const store = storeWrapper.store as Record<string, unknown>;
const taskList = store.taskList as Record<string, Record<string, unknown>>;
const task = taskList[interactionId] as Record<string, unknown> | undefined;
if (!task) return;
Expand All @@ -481,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}
Expand Down
24 changes: 24 additions & 0 deletions playwright/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions playwright/test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '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`,
EMAIL_ENTRY_POINT: `${env.PW_SANDBOX}.e2e10@gmail.com`,
ENTRY_POINT: env.PW_ENTRY_POINT10,
TEST_SUITE: 'campaign-preview-tests.spec.ts',
},
};
8 changes: 8 additions & 0 deletions playwright/test-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,14 @@ export class TestManager {
);
}

async setupForCampaignPreview(browser: Browser): Promise<void> {
await this.setup(browser, {
needsAgent1: true,
agent1LoginMode: LOGIN_MODE.DESKTOP,
enableConsoleLogging: true,
});
}

async setupForMultipartyConference(browser: Browser) {
await this.setup(browser, {
needsAgent1: true,
Expand Down
Loading
Loading