Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(quantic): Exposed clearFilters option in quantic searchboxes #4606

Merged
merged 12 commits into from
Nov 5, 2024
Original file line number Diff line number Diff line change
@@ -1,14 +1,92 @@
/* eslint-disable no-import-assign */
import QuanticSearchBox from 'c/quanticSearchBox';
// @ts-ignore
import {createElement} from 'lwc';
import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';

describe('c-quantic-search-box', () => {
function cleanup() {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
jest.mock('c/quanticHeadlessLoader');

let isInitialized = false;

const exampleEngine = {
id: 'dummy engine',
};

const functionsMocks = {
buildSearchBox: jest.fn(() => ({
state: {},
subscribe: functionsMocks.subscribe,
})),
loadQuerySuggestActions: jest.fn(() => {}),
subscribe: jest.fn((cb) => {
cb();
return functionsMocks.unsubscribe;
}),
unsubscribe: jest.fn(() => {}),
};

const defaultOptions = {
engineId: exampleEngine.id,
placeholder: null,
withoutSubmitButton: false,
numberOfSuggestions: 7,
textarea: false,
disableRecentQueries: false,
keepFiltersOnSearch: false,
};

function createTestComponent(options = defaultOptions) {
prepareHeadlessState();

const element = createElement('c-quantic-search-box', {
is: QuanticSearchBox,
});
for (const [key, value] of Object.entries(options)) {
element[key] = value;
}
document.body.appendChild(element);
return element;
}

function prepareHeadlessState() {
// @ts-ignore
mockHeadlessLoader.getHeadlessBundle = () => {
return {
buildSearchBox: functionsMocks.buildSearchBox,
loadQuerySuggestActions: functionsMocks.loadQuerySuggestActions,
};
};
}

// Helper function to wait until the microtask queue is empty.
function flushPromises() {
// eslint-disable-next-line @lwc/lwc/no-async-operation
return new Promise((resolve) => setTimeout(resolve, 0));
}

function mockSuccessfulHeadlessInitialization() {
// @ts-ignore
mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
if (element instanceof QuanticSearchBox && !isInitialized) {
isInitialized = true;
initialize(exampleEngine);
}
};
}

function cleanup() {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
isInitialized = false;
}

describe('c-quantic-search-box', () => {
beforeAll(() => {
mockSuccessfulHeadlessInitialization();
});

afterEach(() => {
cleanup();
Expand All @@ -19,4 +97,46 @@ describe('c-quantic-search-box', () => {
createElement('c-quantic-search-box', {is: QuanticSearchBox})
).not.toThrow();
});

describe('controller initialization', () => {
it('should subscribe to the headless state changes', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
});

describe('when keepFiltersOnSearch is false (default)', () => {
it('should properly initialize the controller with clear filters enabled', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith(
exampleEngine,
expect.objectContaining({
options: expect.objectContaining({clearFilters: true}),
})
);
});
});

describe('when keepFiltersOnSearch is true', () => {
it('should properly initialize the controller with clear filters disabled', async () => {
createTestComponent({
...defaultOptions,
keepFiltersOnSearch: true,
});
await flushPromises();

expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith(
exampleEngine,
expect.objectContaining({
options: expect.objectContaining({clearFilters: false}),
})
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ export default class QuanticSearchBox extends LightningElement {
* @defaultValue false
*/
@api disableRecentQueries = false;
/**
* Whether to keep all active query filters when the end user submits a new query from the search box.
SimonMilord marked this conversation as resolved.
Show resolved Hide resolved
* @api
* @type {boolean}
* @defaultValue false
*/
@api keepFiltersOnSearch = false;

/** @type {SearchBoxState} */
@track state;
Expand Down Expand Up @@ -100,6 +107,7 @@ export default class QuanticSearchBox extends LightningElement {
close: '</b>',
},
},
clearFilters: !this.keepFiltersOnSearch,
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/* eslint-disable no-import-assign */
import QuanticStandaloneSearchBox from 'c/quanticStandaloneSearchBox';
// @ts-ignore
import {createElement} from 'lwc';
import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
import {CurrentPageReference} from 'lightning/navigation';
import getHeadlessConfiguration from '@salesforce/apex/HeadlessController.getHeadlessConfiguration';

const nonStandaloneURL = 'https://www.example.com/global-search/%40uri';
const defaultHeadlessConfiguration = JSON.stringify({
organization: 'testOrgId',
accessToken: 'testAccessToken',
});

jest.mock('c/quanticHeadlessLoader');

jest.mock(
'@salesforce/apex/HeadlessController.getHeadlessConfiguration',
() => ({
default: jest.fn(),
}),
{virtual: true}
);

mockHeadlessLoader.loadDependencies = () =>
new Promise((resolve) => {
resolve();
});

let isInitialized = false;

const exampleEngine = {
id: 'engineId',
};

const functionsMocks = {
buildStandaloneSearchBox: jest.fn(() => ({
state: {},
subscribe: functionsMocks.subscribe,
})),
subscribe: jest.fn((cb) => {
cb();
return functionsMocks.unsubscribe;
}),
unsubscribe: jest.fn(() => {}),
};

const defaultOptions = {
engineId: exampleEngine.id,
placeholder: null,
withoutSubmitButton: false,
numberOfSuggestions: 7,
textarea: false,
disableRecentQueries: false,
keepFiltersOnSearch: false,
redirectUrl: '/global-search/%40uri',
};

function createTestComponent(options = defaultOptions) {
prepareHeadlessState();
const element = createElement('c-quantic-standalone-search-box', {
is: QuanticStandaloneSearchBox,
});
for (const [key, value] of Object.entries(options)) {
element[key] = value;
}
document.body.appendChild(element);
return element;
}

function prepareHeadlessState() {
// @ts-ignore
global.CoveoHeadless = {
buildStandaloneSearchBox: functionsMocks.buildStandaloneSearchBox,
};
}

// Helper function to wait until the microtask queue is empty.
function flushPromises() {
// eslint-disable-next-line @lwc/lwc/no-async-operation
return new Promise((resolve) => setTimeout(resolve, 0));
}

function mockSuccessfulHeadlessInitialization() {
// @ts-ignore
mockHeadlessLoader.initializeWithHeadless = (element, _, initialize) => {
if (element instanceof QuanticStandaloneSearchBox && !isInitialized) {
isInitialized = true;
initialize(exampleEngine);
}
};
}

function cleanup() {
// The jsdom instance is shared across test cases in a single file so reset the DOM
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
isInitialized = false;
}

describe('c-quantic-standalone-search-box', () => {
beforeEach(() => {
getHeadlessConfiguration.mockResolvedValue(defaultHeadlessConfiguration);
mockSuccessfulHeadlessInitialization();
});

afterEach(() => {
cleanup();
});

it('construct itself without throwing', () => {
expect(() => createTestComponent()).not.toThrow();
});

describe('controller initialization', () => {
it('should subscribe to the headless state changes', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1);
});

describe('when the current page reference changes', () => {
beforeAll(() => {
// This is needed to mock the window.location.href property to test the keepFiltersOnSearch property in the quanticSearchBox.
// https://stackoverflow.com/questions/54021037/how-to-mock-window-location-href-with-jest-vuejs
Object.defineProperty(window, 'location', {
writable: true,
value: {href: nonStandaloneURL},
});
SimonMilord marked this conversation as resolved.
Show resolved Hide resolved
});

it('should properly pass the keepFiltersOnSearch property to the quanticSearchBox', async () => {
const element = createTestComponent({
...defaultOptions,
keepFiltersOnSearch: false,
});
// eslint-disable-next-line @lwc/lwc/no-unexpected-wire-adapter-usages
CurrentPageReference.emit({url: nonStandaloneURL});
await flushPromises();

const searchBox = element.shadowRoot.querySelector(
'c-quantic-search-box'
);

expect(searchBox).not.toBeNull();
expect(searchBox.keepFiltersOnSearch).toEqual(false);
});
});

describe('when keepFiltersOnSearch is false (default)', () => {
it('should properly initialize the controller with clear filters enabled', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes(
1
);
expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith(
exampleEngine,
expect.objectContaining({
options: expect.objectContaining({clearFilters: true}),
})
);
});
});

describe('when keepFiltersOnSearch is true', () => {
it('should properly initialize the controller with clear filters disabled', async () => {
createTestComponent({
...defaultOptions,
keepFiltersOnSearch: true,
});
await flushPromises();

expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledTimes(
1
);
expect(functionsMocks.buildStandaloneSearchBox).toHaveBeenCalledWith(
exampleEngine,
expect.objectContaining({
options: expect.objectContaining({clearFilters: false}),
})
);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin(
* @defaultValue 5
*/
@api numberOfSuggestions = 5;
/**
* Whether to keep all active query filters when the end user submits a new query from the standalone search box.
* @api
* @type {boolean}
* @defaultValue false
*/
@api keepFiltersOnSearch = false;
/**
* The url of the search page to redirect to when a query is made.
* The target search page should contain a `QuanticSearchInterface` with the same engine ID as the one specified for this component.
Expand Down Expand Up @@ -171,6 +178,7 @@ export default class QuanticStandaloneSearchBox extends NavigationMixin(
close: '</b>',
},
},
clearFilters: !this.keepFiltersOnSearch,
redirectionUrl: 'http://placeholder.com',
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
without-submit-button={withoutSubmitButton}
number-of-suggestions={numberOfSuggestions}
textarea={textarea}
keep-filters-on-search={keepFiltersOnSearch}
SimonMilord marked this conversation as resolved.
Show resolved Hide resolved
></c-quantic-search-box>
</template>
</template>
Loading