Skip to content
Draft
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
125 changes: 125 additions & 0 deletions packages/atomic/playwright-utils/analytics-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type {Page} from '@playwright/test';
import type {CapturedRequest} from './api/_base';

/**
* Helper class for accessing captured analytics requests in Playwright e2e tests.
*
* This class provides a bridge between the MSW harness running in the Storybook
* iframe and Playwright tests running outside the iframe.
*
* @example
* ```typescript
* import {AnalyticsHelper} from '@/playwright-utils/analytics-helper';
*
* test('should send analytics', async ({page, productLink}) => {
* await productLink.load();
*
* const analyticsHelper = new AnalyticsHelper(page);
*
* // Clear previous requests
* await analyticsHelper.clearRequests();
*
* // Trigger action
* await productLink.anchor().first().click();
*
* // Wait for and verify analytics
* const request = await analyticsHelper.waitForRequest();
*
* expect(request.body).toMatchObject({
* eventType: 'ec.productClick',
* });
* });
* ```
*/
export class AnalyticsHelper {
constructor(private page: Page) {}

/**
* Clear all captured analytics requests in the iframe.
*/
async clearRequests(): Promise<void> {
await this.page.evaluate(() => {
if (window.__mswAnalyticsHarness) {
window.__mswAnalyticsHarness.eventsEndpoint.clearCapturedRequests();
}
});
}

/**
* Get all captured analytics requests from the iframe.
*/
async getRequests(): Promise<CapturedRequest[]> {
return this.page.evaluate(() => {
if (window.__mswAnalyticsHarness) {
return window.__mswAnalyticsHarness.eventsEndpoint.getCapturedRequests();
}
return [];
});
}

/**
* Get the most recent captured analytics request.
*/
async getLastRequest(): Promise<CapturedRequest | undefined> {
return this.page.evaluate(() => {
if (window.__mswAnalyticsHarness) {
return window.__mswAnalyticsHarness.eventsEndpoint.getLastCapturedRequest();
}
return undefined;
});
}

/**
* Get the count of captured analytics requests.
*/
async getRequestCount(): Promise<number> {
return this.page.evaluate(() => {
if (window.__mswAnalyticsHarness) {
return window.__mswAnalyticsHarness.eventsEndpoint.getCapturedRequestCount();
}
return 0;
});
}

/**
* Wait for the next analytics request to be captured.
* Polls the iframe for new requests.
*
* @param timeout - Maximum time to wait in milliseconds (default: 5000)
* @returns Promise that resolves with the captured request
*/
async waitForRequest(timeout = 5000): Promise<CapturedRequest> {
const startTime = Date.now();
const startCount = await this.getRequestCount();

while (Date.now() - startTime < timeout) {
const currentCount = await this.getRequestCount();
if (currentCount > startCount) {
const lastRequest = await this.getLastRequest();
if (lastRequest) {
return lastRequest;
}
}
// Wait a bit before checking again
await this.page.waitForTimeout(50);
}

throw new Error(`Timeout waiting for analytics request after ${timeout}ms`);
}
}

/**
* Extend the Window interface to include the MSW analytics harness.
*/
declare global {
interface Window {
__mswAnalyticsHarness?: {
eventsEndpoint: {
clearCapturedRequests(): void;
getCapturedRequests(): CapturedRequest[];
getLastCapturedRequest(): CapturedRequest | undefined;
getCapturedRequestCount(): number;
};
};
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
import {html} from 'lit';
import {MockAnalyticsApi} from '@/storybook-utils/api/analytics/mock';
import {wrapInCommerceInterface} from '@/storybook-utils/commerce/commerce-interface-wrapper';
import {wrapInCommerceProductList} from '@/storybook-utils/commerce/commerce-product-list-wrapper';
import {wrapInProductTemplate} from '@/storybook-utils/commerce/commerce-product-template-wrapper';
import {parameters} from '@/storybook-utils/common/common-meta-parameters';

const {decorator: commerceInterfaceDecorator, play} = wrapInCommerceInterface({
type: 'product-listing',
engineConfig: {
context: {
view: {
url: 'https://sports.barca.group/browse/promotions/ui-kit-testing',
// Create analytics harness for e2e test support
const analyticsHarness = new MockAnalyticsApi();

const {decorator: commerceInterfaceDecorator, play: basePlay} =
wrapInCommerceInterface({
type: 'product-listing',
engineConfig: {
context: {
view: {
url: 'https://sports.barca.group/browse/promotions/ui-kit-testing',
},
language: 'en',
country: 'US',
currency: 'USD',
},
preprocessRequest: (request) => {
const parsed = JSON.parse(request.body as string);
parsed.perPage = 1;
request.body = JSON.stringify(parsed);
return request;
},
language: 'en',
country: 'US',
currency: 'USD',
},
preprocessRequest: (request) => {
const parsed = JSON.parse(request.body as string);
parsed.perPage = 1;
request.body = JSON.stringify(parsed);
return request;
},
},
includeCodeRoot: false,
});
includeCodeRoot: false,
});
const {decorator: commerceProductListDecorator} = wrapInCommerceProductList(
'list',
false
Expand All @@ -51,11 +56,21 @@ const meta: Meta = {
actions: {
handles: events,
},
msw: {
handlers: [...analyticsHarness.handlers],
},
},
args,
argTypes,

play,
play: async (context) => {
// Expose analytics harness to window for Playwright e2e tests
if (typeof window !== 'undefined') {
// biome-ignore lint/suspicious/noExplicitAny: window augmentation for Playwright e2e test access
(window as any).__mswAnalyticsHarness = analyticsHarness;
}
await basePlay(context);
},
};

export default meta;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {AnalyticsHelper} from '@/playwright-utils/analytics-helper';
import {expect, test} from './fixture';

test.describe('atomic-product-link', () => {
Expand Down Expand Up @@ -27,4 +28,32 @@ test.describe('atomic-product-link', () => {
// See https://github.com/microsoft/playwright/issues/6479
expect(request).toBeDefined();
});

test('should send ec.productClick event with full payload access (new approach)', async ({
productLink,
page,
}) => {
const analyticsHelper = new AnalyticsHelper(page);

// Clear any previous analytics requests
await analyticsHelper.clearRequests();

// Click the product link
await productLink.anchor().first().click();

// Wait for and verify the analytics request
const request = await analyticsHelper.waitForRequest();

// Now we can access the full payload!
expect(request).toBeDefined();
expect(request.method).toBe('POST');
expect(request.url).toMatch(/analytics\.org\.coveo\.com/);

// Verify the payload structure (example - actual structure may vary)
const payload = request.body as Record<string, unknown>;
expect(payload).toBeDefined();

// Log for demonstration
console.log('Captured analytics payload:', payload);
});
});
Loading
Loading