Skip to content

Commit

Permalink
feat(headless): allow relative redirection urls, communicate state us…
Browse files Browse the repository at this point in the history
  • Loading branch information
samisayegh authored Aug 10, 2021
1 parent a7775be commit 9d8d0cb
Show file tree
Hide file tree
Showing 34 changed files with 862 additions and 158 deletions.
65 changes: 54 additions & 11 deletions packages/atomic/cypress/integration/search-engine.cypress.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
buildSearchEngine,
buildStandaloneSearchBox,
getSampleSearchEngineConfiguration,
loadSearchAnalyticsActions,
SearchEngine,
Expand All @@ -17,22 +18,64 @@ describe('search engine tests', () => {
setupIntercept();
});

it('calling #executeFirstSearch with no arguments logs an interfaceLoad analytics event', () => {
engine.executeFirstSearch();
describe('#executeFirstSearch', () => {
it('when passed no arguments, it logs an interfaceLoad analytics event', () => {
engine.executeFirstSearch();

cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property('actionCause', 'interfaceLoad');
cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property('actionCause', 'interfaceLoad');
});
});

it('when passed an analytics action, it logs the passed action', () => {
const {logSearchFromLink} = loadSearchAnalyticsActions(engine);
engine.executeFirstSearch(logSearchFromLink());

cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property('actionCause', 'searchFromLink');
});
});
});

it('calling #executeFirstSearch with an analytics action logs the passed action', () => {
const {logSearchFromLink} = loadSearchAnalyticsActions(engine);
engine.executeFirstSearch(logSearchFromLink());
describe('#executeFirstSearchAfterStandaloneSearchBoxRedirect', () => {
it(`when a search is executed from a standalone search box,
passing the analytics object logs a searchFromLink`, () => {
const searchBox = buildStandaloneSearchBox(engine, {
options: {redirectionUrl: '/search-page'},
});

searchBox.submit();

engine.executeFirstSearchAfterStandaloneSearchBoxRedirect(
searchBox.state.analytics
);

cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property('actionCause', 'searchFromLink');
});
});

it(`when a suggestion is selected from a standalone search box,
passing the analytics object logs a omniboxFromLink`, () => {
const searchBox = buildStandaloneSearchBox(engine, {
options: {redirectionUrl: '/search-page'},
});

searchBox.selectSuggestion('hello');
engine.executeFirstSearchAfterStandaloneSearchBoxRedirect(
searchBox.state.analytics
);

cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property('actionCause', 'searchFromLink');
cy.wait(RouteAlias.analytics).then(({request}) => {
const analyticsBody = request.body;
expect(analyticsBody).to.have.property(
'actionCause',
'omniboxFromLink'
);
});
});
});
});
6 changes: 3 additions & 3 deletions packages/headless/doc-parser/use-cases/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,6 @@ const actionLoaders: ActionLoaderConfiguration[] = [
{
initializer: 'loadQuestionAnsweringActions',
},
{
initializer: 'loadRedirectionActions',
},
{
initializer: 'loadSearchHubActions',
},
Expand All @@ -429,6 +426,9 @@ const actionLoaders: ActionLoaderConfiguration[] = [
{
initializer: 'loadGenericAnalyticsActions',
},
{
initializer: 'loadStandaloneSearchBoxSetActions',
},
];

const engine: EngineConfiguration = {
Expand Down
2 changes: 1 addition & 1 deletion packages/headless/src/api/search/search-api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {buildMockFacetRequest} from '../../test/mock-facet-request';
import {buildMockCategoryFacetSearch} from '../../test/mock-category-facet-search';
import {buildMockCategoryFacetRequest} from '../../test/mock-category-facet-request';
import {SearchAppState} from '../../state/search-app-state';
import {buildPlanRequest} from '../../features/redirection/redirection-actions';
import {buildQuerySuggestRequest} from '../../features/query-suggest/query-suggest-actions';
import {buildSpecificFacetSearchRequest} from '../../features/facets/facet-search-set/specific/specific-facet-search-request-builder';
import {buildCategoryFacetSearchRequest} from '../../features/facets/facet-search-set/category/category-facet-search-request-builder';
Expand All @@ -34,6 +33,7 @@ import {buildMockAnalyticsState} from '../../test/mock-analytics-state';
import {SearchResponseSuccess} from './search/search-response';
import {emptyQuestionAnswer} from '../../features/search/search-state';
import {QuestionsAnswers} from './search/question-answering';
import {buildPlanRequest} from '../../features/standalone-search-box-set/standalone-search-box-set-actions';

jest.mock('../platform-client');
describe('search api client', () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/headless/src/app/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {resultPreviewReducer} from '../features/result-preview/result-preview-sl
import {searchHubReducer} from '../features/search-hub/search-hub-slice';
import {searchReducer} from '../features/search/search-slice';
import {sortCriteriaReducer} from '../features/sort-criteria/sort-criteria-slice';
import {standaloneSearchBoxSetReducer} from '../features/standalone-search-box-set/standalone-search-box-set-slice';
import {triggerReducer} from '../features/triggers/triggers-slice';
import {undoable} from './undoable';

Expand All @@ -48,6 +49,9 @@ export const categoryFacetSearchSet = categoryFacetSearchSetReducer;
export const query = queryReducer;
export const advancedSearchQueries = advancedSearchQueriesReducer;
export const querySet = querySetReducer;
/**
* @deprecated - The `redirection` functionality is now handled by the `standaloneSearchBoxSet`.
*/
export const redirection = redirectionReducer;
export const querySuggest = querySuggestReducer;
export const sortCriteria = sortCriteriaReducer;
Expand All @@ -61,6 +65,7 @@ export const resultPreview = resultPreviewReducer;
export const version = versionReducer;
export const triggers = triggerReducer;
export const questionAnswering = questionAnsweringReducer;
export const standaloneSearchBoxSet = standaloneSearchBoxSetReducer;

export const history = undoable({
actionTypes: {
Expand Down
28 changes: 27 additions & 1 deletion packages/headless/src/app/search-engine/search-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ import {
getSampleSearchEngineConfiguration,
} from './search-engine-configuration';
import {executeSearch} from '../../features/search/search-actions';
import {logInterfaceLoad} from '../../features/analytics/analytics-actions';
import {
logInterfaceLoad,
logOmniboxFromLink,
logSearchFromLink,
} from '../../features/analytics/analytics-actions';
import {firstSearchExecutedSelector} from '../../features/search/search-selectors';
import {SearchAppState} from '../../state/search-app-state';
import {SearchThunkExtraArguments} from '../search-thunk-extra-arguments';
import {SearchAction} from '../../features/analytics/analytics-utils';
import {StandaloneSearchBoxAnalytics} from '../../features/standalone-search-box-set/standalone-search-box-set-state';

export {
SearchEngineConfiguration,
Expand All @@ -52,6 +57,15 @@ export interface SearchEngine<State extends object = {}>
* @param analyticsEvent - The analytics event to log in association with the first search. If unspecified, `logInterfaceLoad` will be used.
*/
executeFirstSearch(analyticsEvent?: SearchAction): void;

/**
* Executes the first search, and logs the analytics event that triggered a redirection from a standalone search box.
*
* @param analytics - The standalone search box analytics data.
*/
executeFirstSearchAfterStandaloneSearchBoxRedirect(
analytics: StandaloneSearchBoxAnalytics
): void;
}

/**
Expand Down Expand Up @@ -112,6 +126,18 @@ export function buildSearchEngine(options: SearchEngineOptions): SearchEngine {
const action = executeSearch(analyticsEvent);
engine.dispatch(action);
},

executeFirstSearchAfterStandaloneSearchBoxRedirect(
analytics: StandaloneSearchBoxAnalytics
) {
const {cause, metadata} = analytics;
const event =
metadata && cause === 'omniboxFromLink'
? logOmniboxFromLink(metadata)
: logSearchFromLink();

this.executeFirstSearch(event);
},
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/headless/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export {

export {
StandaloneSearchBoxOptions,
StandaloneSearchBoxAnalytics,
StandaloneSearchBoxProps,
StandaloneSearchBoxState,
StandaloneSearchBox,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,5 @@ export const standaloneSearchBoxSchema = new Schema<
redirectionUrl: new StringValue({
required: true,
emptyAllowed: false,
url: true,
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
StandaloneSearchBox,
StandaloneSearchBoxOptions,
} from './headless-standalone-search-box';
import {checkForRedirection} from '../../features/redirection/redirection-actions';
import {createMockState} from '../../test/mock-state';
import {updateQuery} from '../../features/query/query-actions';
import {buildMockQuerySuggest} from '../../test/mock-query-suggest';
Expand All @@ -20,9 +19,19 @@ import {selectQuerySuggestion} from '../../features/query-suggest/query-suggest-
import {
configuration,
query,
redirection,
standaloneSearchBoxSet,
querySuggest,
redirection,
} from '../../app/reducers';
import {
fetchRedirectUrl,
registerStandaloneSearchBox,
updateAnalyticsToOmniboxFromLink,
updateAnalyticsToSearchFromLink,
} from '../../features/standalone-search-box-set/standalone-search-box-set-actions';
import {buildMockStandaloneSearchBoxEntry} from '../../test/mock-standalone-search-box-entry';
import {buildMockOmniboxSuggestionMetadata} from '../../test/mock-omnibox-suggestion-metadata';
import {StandaloneSearchBoxAnalytics} from '../../features/standalone-search-box-set/standalone-search-box-set-state';

describe('headless standalone searchBox', () => {
const id = 'search-box-123';
Expand All @@ -44,9 +53,9 @@ describe('headless standalone searchBox', () => {

function initState() {
state = createMockState();
state.redirection.redirectTo = 'coveo.com';
state.querySet[id] = 'query';
state.querySuggest[id] = buildMockQuerySuggest({id, q: 'some value'});
state.standaloneSearchBoxSet[id] = buildMockStandaloneSearchBoxEntry();
}

function initController() {
Expand All @@ -56,13 +65,22 @@ describe('headless standalone searchBox', () => {

it('it adds the correct reducers to engine', () => {
expect(engine.addReducers).toHaveBeenCalledWith({
redirection,
standaloneSearchBoxSet,
configuration,
query,
querySuggest,
redirection,
});
});

it('dispatches #registerStandaloneSearchBox with the correct options', () => {
const action = registerStandaloneSearchBox({
id,
redirectionUrl: options.redirectionUrl,
});
expect(engine.actions).toContainEqual(action);
});

it('when no id is passed, it creates an id prefixed with standalone_search_box', () => {
options = {redirectionUrl: 'https://www.coveo.com/en/search'};
initController();
Expand All @@ -85,13 +103,18 @@ describe('headless standalone searchBox', () => {
);
});

it('when the redirectionUrl is a relative url, it does not throw', () => {
options.redirectionUrl = '/search-page';
expect(() => initController()).not.toThrow();
});

it('should return the right state', () => {
expect(searchBox.state).toEqual({
value: state.querySet[id],
suggestions: state.querySuggest[id]!.completions.map((completion) => ({
value: completion.expression,
})),
redirectTo: state.redirection.redirectTo,
redirectTo: '',
isLoading: false,
isLoadingSuggestions: false,
analytics: {
Expand All @@ -101,20 +124,44 @@ describe('headless standalone searchBox', () => {
});
});

it('#state.isLoading uses the value in the standalone search-box reducer', () => {
engine.state.standaloneSearchBoxSet[
id
] = buildMockStandaloneSearchBoxEntry({isLoading: true});
expect(searchBox.state.isLoading).toBe(true);
});

it('#state.redirectTo uses the value in the standalone search-box reducer', () => {
const redirectTo = '/search-page';
engine.state.standaloneSearchBoxSet[
id
] = buildMockStandaloneSearchBoxEntry({redirectTo});
expect(searchBox.state.redirectTo).toBe(redirectTo);
});

it('#state.analytics uses the value inside the standalone search-box reducer', () => {
const metadata = buildMockOmniboxSuggestionMetadata();
const analytics: StandaloneSearchBoxAnalytics = {
cause: 'omniboxFromLink',
metadata,
};
engine.state.standaloneSearchBoxSet[
id
] = buildMockStandaloneSearchBoxEntry({analytics});

expect(searchBox.state.analytics).toEqual(analytics);
});

describe('#updateText', () => {
const query = 'a';

beforeEach(() => {
searchBox.updateText(query);
});

it('sets the analytics cause to "searchFromLink"', () => {
searchBox.updateText('');

expect(searchBox.state.analytics).toEqual({
cause: 'searchFromLink',
metadata: null,
});
it('dispatches an action to update analytics to searchFromLink', () => {
const action = updateAnalyticsToSearchFromLink({id});
expect(engine.actions).toContainEqual(action);
});

it('dispatches #updateQuerySetQuery', () => {
Expand All @@ -133,18 +180,18 @@ describe('headless standalone searchBox', () => {
);
});

it('sets #state.analytics to the correct data', () => {
searchBox.selectSuggestion('a');
it('dispatchs an action to update analytics to omniboxFromLink', () => {
const metadata = {
partialQueries: [],
partialQuery: '',
suggestionRanking: -1,
suggestions: [],
};

expect(searchBox.state.analytics).toEqual({
cause: 'omniboxFromLink',
metadata: {
partialQueries: [],
partialQuery: undefined,
suggestionRanking: -1,
suggestions: [],
},
});
const action = updateAnalyticsToOmniboxFromLink({id, metadata});

searchBox.selectSuggestion('a');
expect(engine.actions).toContainEqual(action);
});

it('calls #submit', () => {
Expand All @@ -165,11 +212,11 @@ describe('headless standalone searchBox', () => {
);
});

it('should dispatch a checkForRedirection action', () => {
it('should dispatch a fetchRedirectUrl action', () => {
searchBox.submit();

const action = engine.actions.find(
(a) => a.type === checkForRedirection.pending.type
(a) => a.type === fetchRedirectUrl.pending.type
);
expect(action).toBeTruthy();
});
Expand Down
Loading

0 comments on commit 9d8d0cb

Please sign in to comment.