Skip to content

feat(quantic): quantic insight summary component created #5185

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

Merged
merged 8 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -1610,4 +1610,11 @@
<protected>false</protected>
<shortDescription>Close notification</shortDescription>
</labels>
<labels>
<fullName>quantic_InsightRelatedToThisCase</fullName>
<value>Insights related to this case</value>
<language>en_US</language>
<protected>false</protected>
<shortDescription>Insights related to this case</shortDescription>
</labels>
</CustomLabels>
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/* eslint-disable no-import-assign */
import QuanticInsightSummary from '../quanticInsightSummary';
import * as mockHeadlessLoader from 'c/quanticHeadlessLoader';
import {cleanup, flushPromises, buildCreateTestComponent} from 'c/testUtils';

const initialSummaryLabel = 'Insights related to this cases';

let isInitialized = false;

jest.mock('c/quanticHeadlessLoader');
jest.mock(
'@salesforce/label/c.quantic_InsightRelatedToThisCase',
() => ({default: initialSummaryLabel}),
{
virtual: true,
}
);

const initialSearchBoxState = {
value: '',
};
let searchBoxState = initialSearchBoxState;

const initialQuerySummaryState = {hasError: false};
let querySummaryState = initialQuerySummaryState;

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

const defaultOptions = {
engineId: exampleEngine.id,
};

const functionsMocks = {
buildSearchBox: jest.fn(() => ({
state: searchBoxState,
subscribe: functionsMocks.searchBoxStateSubscriber,
})),
buildQuerySummary: jest.fn(() => ({
state: querySummaryState,
subscribe: functionsMocks.querySummaryStateSubscriber,
})),
searchBoxStateSubscriber: jest.fn((cb) => {
cb();
return functionsMocks.searchBoxStateUnsubscriber;
}),
querySummaryStateSubscriber: jest.fn((cb) => {
cb();
return functionsMocks.querySummaryStateUnsubscriber;
}),
searchBoxStateUnsubscriber: jest.fn(),
querySummaryStateUnsubscriber: jest.fn(),
};

const selectors = {
initializationError: 'c-quantic-component-error',
initialSummaryMessage: '[data-testid="initial-summary-message"]',
quanticSummary: 'c-quantic-summary',
};

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

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

function mockErroneousHeadlessInitialization() {
// @ts-ignore
mockHeadlessLoader.initializeWithHeadless = (element) => {
if (element instanceof QuanticInsightSummary) {
element.setInitializationError();
}
};
}

const createTestComponent = buildCreateTestComponent(
QuanticInsightSummary,
'c-quantic-insight-summary',
{defaultOptions}
);

describe('c-quantic-insight-summary', () => {
beforeEach(() => {
mockSuccessfulHeadlessInitialization();
prepareHeadlessState();
});

afterEach(() => {
cleanup();
searchBoxState = initialSearchBoxState;
querySummaryState = initialQuerySummaryState;
isInitialized = false;
});

describe('component initialization', () => {
it('should initialize the search box and query summary headless controllers', async () => {
createTestComponent();
await flushPromises();

expect(functionsMocks.buildSearchBox).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildSearchBox).toHaveBeenCalledWith(exampleEngine);
expect(functionsMocks.buildQuerySummary).toHaveBeenCalledTimes(1);
expect(functionsMocks.buildQuerySummary).toHaveBeenCalledWith(
exampleEngine
);
});

it('should subscribe the search box and query summary state changes', async () => {

Check warning on line 123 in packages/quantic/force-app/main/default/lwc/quanticInsightSummary/__tests__/quanticInsightSummary.test.js

View workflow job for this annotation

GitHub Actions / Check with linter

Test has no assertions
createTestComponent();
await flushPromises();
});
});

describe('component disconnection', () => {
it('should unsubscribe from the search box and query summary state changes', async () => {
const element = createTestComponent();
await flushPromises();

document.body.removeChild(element);

expect(functionsMocks.searchBoxStateUnsubscriber).toHaveBeenCalledTimes(
1
);
expect(
functionsMocks.querySummaryStateUnsubscriber
).toHaveBeenCalledTimes(1);
});
});

describe('when an initialization error occurs', () => {
beforeEach(() => {
mockErroneousHeadlessInitialization();
});

it('should display the initialization error component', async () => {
const element = createTestComponent();
await flushPromises();

const initializationError = element.shadowRoot.querySelector(
selectors.initializationError
);

expect(initializationError).not.toBeNull();
});
});

describe('when no search query is executed', () => {
it('should display the default insight panel message', async () => {
const element = createTestComponent();
await flushPromises();

const initialSummaryMessage = element.shadowRoot.querySelector(
selectors.initialSummaryMessage
);
const quanticQuerySummary = element.shadowRoot.querySelector(
selectors.quanticSummary
);

expect(initialSummaryMessage).not.toBeNull();
expect(initialSummaryMessage.textContent).toBe(initialSummaryLabel);
expect(quanticQuerySummary).toBeNull();
});
});

describe('when a search query is executed', () => {
beforeEach(() => {
searchBoxState = {
value: 'test',
};
});

it('should display the quantic summary component', async () => {
const element = createTestComponent();
await flushPromises();

const initialSummaryMessage = await element.shadowRoot.querySelector(
selectors.initialSummaryMessage
);
const quanticQuerySummary = await element.shadowRoot.querySelector(
selectors.quanticSummary
);

expect(quanticQuerySummary).not.toBeNull();
expect(initialSummaryMessage).toBeNull();
});
});

describe('when there is a query error', () => {
beforeEach(() => {
querySummaryState = {
hasError: true,
};
});

it('should not display the Insight Panel Summary component', async () => {
const element = createTestComponent();
await flushPromises();

const title = await element.shadowRoot.querySelector(
selectors.initialSummaryMessage
);
const quanticQuerySummary = await element.shadowRoot.querySelector(
selectors.quanticSummary
);

expect(quanticQuerySummary).toBeNull();
expect(title).toBeNull();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.initial-summary-message {
background-color: #d8e6fe;
color: #54698d;
width: 100%;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<template lwc:if={hasInitializationError}>
<c-quantic-component-error component-name={template.host.localName}>
</c-quantic-component-error>
</template>
<template lwc:else>
<template if:false={hasError}>
<template if:false={showQuerySummary}>
<div data-testid="initial-summary-message" class="slds-text-title_bold initial-summary-message slds-var-p-vertical_small slds-var-p-horizontal_medium">{labels.insightRelatedToThisCase}</div>
</template>
<template if:true={showQuerySummary}>
<div class="slds-var-p-vertical_small slds-var-p-horizontal_medium">
<c-quantic-summary engine-id={engineId}></c-quantic-summary>
</div>
</template>
</template>
</template>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {LightningElement, api} from 'lwc';
import {
registerComponentForInit,
initializeWithHeadless,
getHeadlessBundle,
} from 'c/quanticHeadlessLoader';
import insightRelatedToThisCase from '@salesforce/label/c.quantic_InsightRelatedToThisCase';

/** @typedef {import("coveo").InsightEngine} InsightEngine */
/** @typedef {import("coveo").SearchBox} SearchBox */
/** @typedef {import("coveo").QuerySummary} QuerySummary */

/**
* The `QuanticInsightPanelSummary` component is a composite component that displays a title or a Quantic Query Summary depending whether a user query is performed.
* @category Insight Panel
* @example
* <c-quantic-insight-query-summary engine-id={engineId}></c-quantic-insight-query-summary>
*/
export default class QuanticInsightPanelSummary extends LightningElement {
/**
* The ID of the engine instance the component registers to.
* @api
* @type {string}
*/
@api engineId;

labels = {
insightRelatedToThisCase,
};

/** @type {boolean} */
showQuerySummary = false;
/** @type {boolean} */
hasError = false;

/** @type {AnyHeadless} */
headless;
/** @type {SearchBox} */
searchBox;
/** @type {QuerySummary} */
querySummary;
/** @type {boolean} */
hasInitializationError;

connectedCallback() {
registerComponentForInit(this, this.engineId);
}

renderedCallback() {
initializeWithHeadless(this, this.engineId, this.initialize);
}

/**
* @param {InsightEngine} engine
*/
initialize = (engine) => {
this.headless = getHeadlessBundle(this.engineId);
this.searchBox = this.headless.buildSearchBox(engine);
this.querySummary = this.headless.buildQuerySummary(engine);
this.unsubscribeSearchBox = this.searchBox.subscribe(() =>
this.updateSummaryVisibility()
);
this.unsubscribeQuerySummary = this.querySummary.subscribe(() =>
this.updateHasError()
);
};

disconnectedCallback() {
this.unsubscribeSearchBox?.();
this.unsubscribeQuerySummary?.();
}

/**
* Updates summary visibility.
* @returns {void}
*/
updateSummaryVisibility() {
this.showQuerySummary = !!this.searchBox.state.value;
}

/**
* updates the error state of the component.
* @returns {void}
*/
updateHasError() {
this.hasError = this.querySummary?.state?.hasError;
}

/**
* Sets the component in the initialization error state.
*/
setInitializationError() {
this.hasInitializationError = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>60.0</apiVersion>
<isExposed>false</isExposed>
</LightningComponentBundle>
Original file line number Diff line number Diff line change
Expand Up @@ -884,4 +884,8 @@
<label>Fermer la notification</label>
<name>quantic_CloseNotification</name>
</customLabels>
<customLabels>
<label>Perspectives liées à ce cas</label>
<name>quantic_InsightRelatedToThisCase</name>
</customLabels>
</Translations>
Loading