Skip to content
Merged
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
12 changes: 9 additions & 3 deletions src/hooks/useDownShift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useCombobox, UseComboboxProps, UseComboboxReturnValue } from 'downshift
import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
import { Nullable } from '@constructor-io/constructorio-client-javascript/lib/types';
import { Item, OnSubmit } from '../types';
import { trackSearchSubmit, trackAutocompleteSelect } from '../utils/tracking';
import {
trackSearchSubmit,
trackAutocompleteSelect,
trackRecommendationSelect,
} from '../utils/tracking';

let idCounter = 0;

Expand Down Expand Up @@ -44,15 +48,17 @@ const useDownShift: UseDownShift = ({
// Autocomplete Select tracking
// Recommendation Select tracking
if (selectedItem.podId && selectedItem.data?.id && selectedItem.strategy) {
cioClient?.tracker.trackRecommendationClick({
const recommendationData = {
itemName: selectedItem.value,
itemId: selectedItem.data.id,
variationId: selectedItem.data.variation_id,
podId: selectedItem.podId,
strategyId: selectedItem.strategy.id,
section: selectedItem.section,
resultId: selectedItem.result_id,
});
};
trackRecommendationSelect(cioClient, recommendationData);

// Select tracking for all other Constructor sections:
// (ie: Search Suggestions, Products, Custom Cio sections, etc)
// This does not apply to custom user defined sections that aren't part of Constructor index
Expand Down
63 changes: 59 additions & 4 deletions src/stories/tests/ComponentTests.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { storageGetItem, storageSetItem } from '../../utils/storage';
import { ComponentTemplate } from '../Autocomplete/Component';
import { apiKey, onSubmitDefault as onSubmit } from '../../constants';
import { CioAutocompleteProps } from '../../types';
import { isTrackingRequestSent } from '../../utils/tracking';
import { isTrackingRequestSent, captureTrackingRequest } from '../../utils/tracking';
import { CONSTANTS } from '../../utils/beaconUtils';

export default {
Expand Down Expand Up @@ -76,8 +76,13 @@ FocusFiresTrackingEvent.args = defaultArgs;
FocusFiresTrackingEvent.play = async ({ canvasElement }) => {
await sleep(100);
const canvas = within(canvasElement);
await userEvent.click(canvas.getByTestId('cio-input'));
const isFocusTrackingRequestSent = isTrackingRequestSent('action=focus');
const input = canvas.getByTestId('cio-input');

// Use the reusable tracking capture utility
const isFocusTrackingRequestSent = await captureTrackingRequest('action=focus', async () => {
await userEvent.click(input);
});

expect(isFocusTrackingRequestSent).toBeTruthy();
};

Expand Down Expand Up @@ -357,6 +362,56 @@ SelectProductSuggestionClearsSearchTermStorage.play = async ({ canvasElement })
await sleep(1000);
};

// - select recommendation from zero state => Search Term Storage is Cleared
export const SelectZeroStateRecommendationClearsSearchTermStorage = ComponentTemplate.bind({});
SelectZeroStateRecommendationClearsSearchTermStorage.args = {
...defaultArgs,
autocompleteClassName: 'cio-autocomplete full-example-autocomplete-styles',
advancedParameters: {
displaySearchSuggestionImages: true,
displaySearchSuggestionResultCounts: true,
numTermsWithGroupSuggestions: 6,
},
sections: [
{
indexSectionName: 'Search Suggestions',
numResults: 8,
displaySearchTermHighlights: true,
},
],
zeroStateSections: [
{
podId: 'bestsellers',
type: 'recommendations',
numResults: 6,
},
],
};
SelectZeroStateRecommendationClearsSearchTermStorage.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
storageSetItem(CONSTANTS.SEARCH_TERM_STORAGE_KEY, 'test search term');

await userEvent.click(canvas.getByTestId('cio-input'));
await sleep(1000);
expect(canvas.getAllByText('Best Sellers').length).toBeGreaterThan(0);

const bestSellersSection = canvas.getByTestId('cio-results').querySelector('.cio-section');
const recommendationItems = bestSellersSection?.querySelectorAll('[data-cnstrc-item-id]');

const firstRecommendation = recommendationItems?.[0];
const isSelectTrackingRequestSent = await captureTrackingRequest(
'/recommendation_result_click',
async () => {
if (firstRecommendation) {
await userEvent.click(firstRecommendation);
}
}
);

expect(isSelectTrackingRequestSent).toBeTruthy();
expect(storageGetItem(CONSTANTS.SEARCH_TERM_STORAGE_KEY)).toBeNull();
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this particular test is created to ensure that search_term_storage is cleared, but should we also check if the tracking event is fired using that new utility captureTrackingRequest?

Copy link
Contributor Author

@Alexey-Pavlov Alexey-Pavlov Oct 8, 2025

Choose a reason for hiding this comment

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

Yep, we can, without any problems
Added this check ✅

Thank you for the review!

};

// - click search icon => network search submit event
export const SearchIconSubmitSearch = ComponentTemplate.bind({});
SearchIconSubmitSearch.args = defaultArgs;
Expand Down Expand Up @@ -505,7 +560,7 @@ InGroupSuggestions.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('cio-input'), 'socks', { delay: 100 });
await sleep(1000);
expect(canvas.getAllByText('in Socks').length).toEqual(1);
expect(canvas.getAllByText(/in Socks/)).toHaveLength(1);
Comment on lines -508 to +563
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for fixing this

};

export const InGroupSuggestionsTwo = ComponentTemplate.bind({});
Expand Down
15 changes: 10 additions & 5 deletions src/stories/tests/HooksTests.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { within, userEvent, expect, fn } from '@storybook/test';
import { CioAutocomplete } from '../../index';
import { argTypes } from '../Autocomplete/argTypes';
import { getCioClient, sleep } from '../../utils/helpers';
import { isTrackingRequestSent } from '../../utils/tracking';
import { HooksTemplate } from '../Autocomplete/Hook/index';
import { isTrackingRequestSent, captureTrackingRequest } from '../../utils/tracking';
import { HooksTemplate } from '../Autocomplete/Hook';
import { apiKey, onSubmitDefault as onSubmit } from '../../constants';
import { CioAutocompleteProps } from '../../types';

Expand Down Expand Up @@ -73,8 +73,13 @@ FocusFiresTrackingEvent.args = defaultArgs;
FocusFiresTrackingEvent.play = async ({ canvasElement }) => {
await sleep(100);
const canvas = within(canvasElement);
await userEvent.click(canvas.getByTestId('cio-input'));
const isFocusTrackingRequestSent = isTrackingRequestSent('action=focus');
const input = canvas.getByTestId('cio-input');

// Use the reusable tracking capture utility
const isFocusTrackingRequestSent = await captureTrackingRequest('action=focus', async () => {
await userEvent.click(input);
});

expect(isFocusTrackingRequestSent).toBeTruthy();
};

Expand Down Expand Up @@ -435,7 +440,7 @@ InGroupSuggestions.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('cio-input'), 'socks', { delay: 100 });
await sleep(1000);
expect(canvas.getAllByText('in Socks').length).toEqual(1);
expect(canvas.getAllByText(/in Socks/)).toHaveLength(1);
};

export const InGroupSuggestionsTwo = HooksTemplate.bind({});
Expand Down
33 changes: 33 additions & 0 deletions src/utils/tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,36 @@ export const trackAutocompleteSelect = (cioClient, itemName, autocompleteData: a
storageRemoveItem(CONSTANTS.SEARCH_TERM_STORAGE_KEY);
}
};

export const trackRecommendationSelect = (cioClient, recommendationData: any = {}) => {
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

The recommendationData parameter uses any type which reduces type safety. Consider defining a proper interface for recommendation data to improve type checking and developer experience.

Suggested change
export const trackRecommendationSelect = (cioClient, recommendationData: any = {}) => {
interface RecommendationData {
podId?: string;
numResultsViewed?: number;
url?: string;
section?: string;
items?: Array<{
itemId?: string;
itemName?: string;
variationId?: string;
}>;
// Add other properties as needed
}
export const trackRecommendationSelect = (
cioClient: Nullable<ConstructorIOClient>,
recommendationData: RecommendationData = {}
) => {

Copilot uses AI. Check for mistakes.
storageRemoveItem(CONSTANTS.SEARCH_TERM_STORAGE_KEY);
cioClient?.tracker.trackRecommendationClick(recommendationData);
};
Comment on lines +59 to +62
Copy link
Contributor

Choose a reason for hiding this comment

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

good abstraction


export const captureTrackingRequest = async (
urlPattern: string,
action: () => Promise<void>
): Promise<boolean> => {
let trackingCaptured = false;
const originalSetItem = Storage.prototype.setItem;

Storage.prototype.setItem = function setItemInterceptor(key, value) {
Comment on lines +69 to +71
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

Modifying the global Storage.prototype could cause issues if multiple components use this function simultaneously or if the restoration fails. Consider using a more isolated approach or adding proper error handling to ensure the prototype is always restored.

Copilot uses AI. Check for mistakes.
if (key === '_constructorio_requests') {
try {
const requests = JSON.parse(value);
if (requests.some((req: any) => req?.url?.includes(urlPattern))) {
Copy link

Copilot AI Oct 8, 2025

Choose a reason for hiding this comment

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

Using any type for request objects reduces type safety. Consider defining a proper interface for request objects to improve type checking.

Copilot uses AI. Check for mistakes.
trackingCaptured = true;
}
} catch (e) {
// Ignore parsing errors
}
}
return originalSetItem.call(this, key, value);
};

await action();

Storage.prototype.setItem = originalSetItem;

return trackingCaptured || isTrackingRequestSent(urlPattern);
};
Loading