From 7782c301fb3174ff4b9bbbf44970775f33d4904b Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Fri, 13 Nov 2020 16:56:59 -0500 Subject: [PATCH 1/8] feat(SFINT-3521): Add AugmentedResultList --- src/Index.ts | 1 + .../AugmentedResultList.ts | 142 +++++++++++++++ .../AugmentedResultList.spec.ts | 162 ++++++++++++++++++ tests/models/UserProfilingModel.spec.ts | 1 - 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/components/AugmentedResultList/AugmentedResultList.ts create mode 100644 tests/components/AugmentedResultList/AugmentedResultList.spec.ts diff --git a/src/Index.ts b/src/Index.ts index 52765275..34ba3173 100644 --- a/src/Index.ts +++ b/src/Index.ts @@ -15,3 +15,4 @@ export { ViewedByCustomer } from './components/ViewedByCustomer/ViewedByCustomer export { ResultAction } from './components/ResultAction/ResultAction'; export { CopyToClipboard } from './components/CopyToClipboard/CopyToClipboard'; export { TopQueries } from './components/TopQueries/TopQueries'; +export { AugmentedResultList } from './components/AugmentedResultList/AugmentedResultList'; diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts new file mode 100644 index 00000000..a42011af --- /dev/null +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -0,0 +1,142 @@ +import { $$, ComponentOptions, IComponentBindings, Initialization, IQueryResult, IResultListOptions, IFieldOption } from 'coveo-search-ui'; + +/** + * Interface for the object data returned from remote action. + * + * @export + * @interface IObjectData + */ +export interface IObjectData { + /** + * Data specific to a result with matching object id. + */ + objectData: {}[]; + /** + * Data to add to every result with matching object id. + */ + commonData: {}; +} + +/** + * Generic interface for the response returned by the remote action method. + * + * @export + * @interface IPromiseReturnArgs + * @template T + */ +export interface IPromiseReturnArgs { + data: IObjectData; +} + +export interface AugmentedResultListOptions extends IResultListOptions { + matchingIdField: IFieldOption; + objectDataAction: (objectIds: String[]) => Promise; +} + +export class AugmentedResultList extends Coveo.ResultList implements IComponentBindings { + static ID = 'AugmentedResultList'; + + /** + * @componentOptions + */ + static options: AugmentedResultListOptions = { + /** + * The field to be used as matching ID between object and result. + */ + matchingIdField: ComponentOptions.buildFieldOption({ + required: true, + }), + /** + * The function used to fetch extra object information. + */ + objectDataAction: ComponentOptions.buildCustomOption<(objectIds: String[]) => Promise>(() => { + return null; + }), + }; + + /** + * Creates a new `AugmentedResultList` component. + * @param element The HTMLElement on which to instantiate the component. + * @param options The options for the `ResultList` component. + * @param bindings The bindings that the component requires to function normally. If not set, these will be + * automatically resolved (with a slower execution time). + */ + constructor(public element: HTMLElement, public options: AugmentedResultListOptions, public bindings: IComponentBindings) { + super(element, ComponentOptions.initComponentOptions(element, AugmentedResultList, options), bindings, AugmentedResultList.ID); + } + + private getObjectPayload(results: IQueryResult[]): String[] { + const field = this.getFieldString(this.options.matchingIdField); + if (results.length > 0) { + return results.filter((result) => result.raw && result.raw[field]).map((result) => result.raw[field]); + } + return []; + } + + private getFieldString(fieldName: IFieldOption) { + return fieldName.replace('@', ''); + } + + protected enableAnimation() { + if (!document.getElementById('overlay')) { + $$(document.body).append($$('div', { id: 'overlay', class: 'modal-backdrop fade in' }).el); + } + } + + protected disableAnimation() { + if (document.getElementById('overlay')) { + document.getElementById('overlay').remove(); + } + } + + public renderResults(resultElements: HTMLElement[], append = false): Promise { + const res = super.renderResults(resultElements, append); + this.disableAnimation(); + return res; + } + + public async buildResults(results: Coveo.IQueryResults): Promise { + this.enableAnimation(); + const fieldString = this.getFieldString(this.options.matchingIdField); + + if (this.options.objectDataAction) { + // Call remote action to fetch object data + const remoteResults: IPromiseReturnArgs = await this.options + .objectDataAction(this.getObjectPayload(results.results)) + .then((data) => { + return data; + }) + .catch((ex) => { + this.logger.error('Unable to fetch object data.'); + return null; + }); + + if (remoteResults && remoteResults.data) { + // Merge remote action results with Coveo Results + results.results.forEach((res: Coveo.IQueryResult) => { + const match = remoteResults.data.objectData.find((data) => { + return (data as any)[fieldString] === res.raw[fieldString]; + }); + + // Attach data specific to each result/object + for (const key in match) { + res.raw[key.toLowerCase()] = (match as any)[key]; + } + + // Attach data common to all results + for (const key in remoteResults.data.commonData) { + res.raw[key.toLowerCase()] = (remoteResults.data.commonData as any)[key]; + } + }); + } + } else { + this.logger.error('No objectDataAction is defined.'); + } + + const ret = super.buildResults(results); + return ret; + } +} + +Initialization.registerAutoCreateComponent(AugmentedResultList); +Initialization.registerComponentFields(AugmentedResultList.ID, [String(AugmentedResultList.options.matchingIdField)]); diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts new file mode 100644 index 00000000..fdc5cb2f --- /dev/null +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -0,0 +1,162 @@ +import * as sinon from 'sinon'; +import { IQueryResult, Logger, SearchEndpoint } from 'coveo-search-ui'; +import { Mock, Fake } from 'coveo-search-ui-tests'; +import { AugmentedResultList, IPromiseReturnArgs } from '../../../src/components/AugmentedResultList/AugmentedResultList'; + +describe('AugmentedResultList', () => { + let sandbox: sinon.SinonSandbox; + + let componentSetup: Mock.IBasicComponentSetup; + let element: HTMLElement; + let testOptions: Mock.AdvancedComponentSetupOptions; + + const objectData = [ + { + id: '#001', + name: 'bulbasaur', + type: 'grass,poison', + }, + { + id: '#004', + name: 'charmander', + type: 'fire', + }, + { + id: '#007', + name: 'squirtle', + type: 'water', + }, + ]; + + const commonData = { + evolution: 1, + starter: true, + }; + + const returnData: IPromiseReturnArgs = { + data: { + objectData, + commonData, + }, + }; + + const matchingIdField = '@id'; + + const stubObjectAction = (objectIds: string[]): Promise => { + return Promise.resolve(returnData); + }; + + const createFakeResultsThatMatch = (numResults: number) => { + const fakeResults = Fake.createFakeResults(numResults); + fakeResults.results.forEach((result, index) => (result.raw.id = `#00${index}`)); + return fakeResults; + }; + + const createComponent = (objectDataAction?: (objectIds: string[]) => Promise) => { + element = document.createElement('div'); + document.body.append(element); + testOptions = new Mock.AdvancedComponentSetupOptions(element, { + matchingIdField, + objectDataAction, + }); + + componentSetup = Mock.advancedComponentSetup(AugmentedResultList, testOptions); + return componentSetup; + }; + + const verifyAugmentedObjectData = (objectData: any, results: IQueryResult[]) => { + objectData.forEach((object: any) => { + const matchingResult = results.find((result) => result.raw.id === (object as any).id); + if (matchingResult) { + for (const key in object) { + expect(matchingResult.raw[key]).toEqual(object[key]); + } + } + }); + }; + + const verifyUntouchedResults = (objectData: {}[], results: IQueryResult[]) => { + const idString = matchingIdField.replace('@', ''); + + objectData.forEach((object: any) => { + const ids = objectData.map((data: any) => (data as any)[idString]); + const otherResults = results.filter((result) => !ids.find((id) => (result as any)[idString] === id)); + + otherResults.forEach((result: IQueryResult) => { + for (const key in object) { + if (key !== idString) { + expect(result.raw[key]).toBeUndefined; + } + } + }); + }); + }; + + const verifyAugmentedCommonData = (commonData: any, results: IQueryResult[]) => { + results.forEach((result) => { + for (const key in commonData) { + expect(result.raw[key]).toEqual(commonData[key]); + } + }); + }; + + const verifyAugmentedResults = (returnData: IPromiseReturnArgs, results: IQueryResult[]) => { + verifyAugmentedObjectData(returnData.data.objectData, results); + verifyAugmentedCommonData(returnData.data.commonData, results); + verifyUntouchedResults(returnData.data.objectData, results); + }; + + beforeAll(() => { + Logger.disable(); + }); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const endpoint = sandbox.createStubInstance(SearchEndpoint); + endpoint.search.callsFake(() => Promise.resolve(createFakeResultsThatMatch(10))); + }); + + afterEach(() => { + sandbox.restore(); + }); + + afterAll(() => { + Logger.enable(); + }); + + it('should augment results with object data', (done) => { + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(stubObjectAction); + + test.cmp.buildResults(data).then(() => { + expect(test.cmp.getDisplayedResults().length).toEqual(numResults); + verifyAugmentedResults(returnData, test.cmp.getDisplayedResults()); + done(); + }); + }); + + it("should NOT augment results if IDs don't match", (done) => { + const numResults = 10; + const data = Fake.createFakeResults(numResults); + const test = createComponent(stubObjectAction); + + test.cmp.buildResults(data).then(() => { + expect(test.cmp.getDisplayedResults().length).toEqual(numResults); + verifyAugmentedResults(returnData, test.cmp.getDisplayedResults()); + done(); + }); + }); + + it('should still build results without augmenting if objectDataAction is missing', (done) => { + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(); + + test.cmp.buildResults(data).then(() => { + expect(test.cmp.getDisplayedResults().length).toEqual(numResults); + expect(test.cmp.getDisplayedResults()).toEqual(data.results); + done(); + }); + }); +}); diff --git a/tests/models/UserProfilingModel.spec.ts b/tests/models/UserProfilingModel.spec.ts index bda9581d..4a3ad92d 100644 --- a/tests/models/UserProfilingModel.spec.ts +++ b/tests/models/UserProfilingModel.spec.ts @@ -172,7 +172,6 @@ describe('UserProfilingModel', () => { ); const actions = await actionsPromise; - console.log(actions); const actionsWithDocument = actions.filter((action) => action.document); const uniqueUriHashes = FAKE_ACTIONS_WITH_URI_HASH.map((x) => x.value.uri_hash).filter((x, i, l) => l.indexOf(x) === i); From 05c1b881ee254885231fbde256c4199ca7d7db68 Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Fri, 13 Nov 2020 16:57:33 -0500 Subject: [PATCH 2/8] feat(SFINT-3521): linting --- .../AugmentedResultList.ts | 2 +- .../AugmentedResultList.spec.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts index a42011af..881fa5d0 100644 --- a/src/components/AugmentedResultList/AugmentedResultList.ts +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -120,7 +120,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB // Attach data specific to each result/object for (const key in match) { - res.raw[key.toLowerCase()] = (match as any)[key]; + res.raw[key.toLowerCase()] = (match as any)[key]; } // Attach data common to all results diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index fdc5cb2f..c9263409 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -149,14 +149,14 @@ describe('AugmentedResultList', () => { }); it('should still build results without augmenting if objectDataAction is missing', (done) => { - const numResults = 10; - const data = createFakeResultsThatMatch(numResults); - const test = createComponent(); - - test.cmp.buildResults(data).then(() => { - expect(test.cmp.getDisplayedResults().length).toEqual(numResults); - expect(test.cmp.getDisplayedResults()).toEqual(data.results); - done(); - }); - }); + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(); + + test.cmp.buildResults(data).then(() => { + expect(test.cmp.getDisplayedResults().length).toEqual(numResults); + expect(test.cmp.getDisplayedResults()).toEqual(data.results); + done(); + }); + }); }); From 0fe7031526c416a9057b987b21f4e1821b2fdbaa Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Mon, 16 Nov 2020 10:32:00 -0500 Subject: [PATCH 3/8] feat(SFINT-3521): removed unused overlay functions and renaming --- .../AugmentedResultList.ts | 73 ++++++++----------- .../AugmentedResultList.spec.ts | 60 +++++++++------ 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts index 881fa5d0..b797fb50 100644 --- a/src/components/AugmentedResultList/AugmentedResultList.ts +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -1,36 +1,36 @@ -import { $$, ComponentOptions, IComponentBindings, Initialization, IQueryResult, IResultListOptions, IFieldOption } from 'coveo-search-ui'; +import { ComponentOptions, IComponentBindings, Initialization, IQueryResult, IResultListOptions, IFieldOption } from 'coveo-search-ui'; /** - * Interface for the object data returned from remote action. + * Interface for the data returned from external fetch action. * * @export - * @interface IObjectData + * @interface IAugmentData */ -export interface IObjectData { +export interface IAugmentData { /** * Data specific to a result with matching object id. */ - objectData: {}[]; + resultData: any[]; /** * Data to add to every result with matching object id. */ - commonData: {}; + commonData: any; } /** - * Generic interface for the response returned by the remote action method. + * Generic interface for the response returned by the external fetch action. * * @export * @interface IPromiseReturnArgs * @template T */ -export interface IPromiseReturnArgs { - data: IObjectData; +export interface IPromiseReturnArgs { + data: T; } export interface AugmentedResultListOptions extends IResultListOptions { matchingIdField: IFieldOption; - objectDataAction: (objectIds: String[]) => Promise; + fetchAugmentData: (objectIds: String[]) => Promise>; } export class AugmentedResultList extends Coveo.ResultList implements IComponentBindings { @@ -41,15 +41,15 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB */ static options: AugmentedResultListOptions = { /** - * The field to be used as matching ID between object and result. + * The field to be used as matching ID between augment data and result. */ matchingIdField: ComponentOptions.buildFieldOption({ required: true, }), /** - * The function used to fetch extra object information. + * The function used to fetch extra result information. */ - objectDataAction: ComponentOptions.buildCustomOption<(objectIds: String[]) => Promise>(() => { + fetchAugmentData: ComponentOptions.buildCustomOption<(objectIds: String[]) => Promise>>(() => { return null; }), }; @@ -77,49 +77,36 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB return fieldName.replace('@', ''); } - protected enableAnimation() { - if (!document.getElementById('overlay')) { - $$(document.body).append($$('div', { id: 'overlay', class: 'modal-backdrop fade in' }).el); - } - } - - protected disableAnimation() { - if (document.getElementById('overlay')) { - document.getElementById('overlay').remove(); - } - } - public renderResults(resultElements: HTMLElement[], append = false): Promise { const res = super.renderResults(resultElements, append); - this.disableAnimation(); return res; } - public async buildResults(results: Coveo.IQueryResults): Promise { - this.enableAnimation(); + public async buildResults(queryResults: Coveo.IQueryResults): Promise { const fieldString = this.getFieldString(this.options.matchingIdField); - if (this.options.objectDataAction) { - // Call remote action to fetch object data - const remoteResults: IPromiseReturnArgs = await this.options - .objectDataAction(this.getObjectPayload(results.results)) - .then((data) => { - return data; - }) - .catch((ex) => { - this.logger.error('Unable to fetch object data.'); - return null; - }); + if (this.options.fetchAugmentData) { + let remoteResults: IPromiseReturnArgs; + try { + // Call remote action to fetch augmenting data + remoteResults = await this.options.fetchAugmentData(this.getObjectPayload(queryResults.results)); + } catch (e) { + this.logger.error(['Unable to fetch augment data.', e]); + return null; + } - if (remoteResults && remoteResults.data) { + if (remoteResults?.data) { // Merge remote action results with Coveo Results - results.results.forEach((res: Coveo.IQueryResult) => { - const match = remoteResults.data.objectData.find((data) => { + queryResults.results.forEach((res: Coveo.IQueryResult) => { + const match = remoteResults.data.resultData.find((data) => { return (data as any)[fieldString] === res.raw[fieldString]; }); // Attach data specific to each result/object for (const key in match) { + if (Boolean(res.raw[key.toLowerCase()])) { + this.logger.warn(`The ${key} field was overwritten on result: ${res.title}`); + } res.raw[key.toLowerCase()] = (match as any)[key]; } @@ -133,7 +120,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB this.logger.error('No objectDataAction is defined.'); } - const ret = super.buildResults(results); + const ret = super.buildResults(queryResults); return ret; } } diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index c9263409..1732bfef 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -1,7 +1,7 @@ import * as sinon from 'sinon'; import { IQueryResult, Logger, SearchEndpoint } from 'coveo-search-ui'; import { Mock, Fake } from 'coveo-search-ui-tests'; -import { AugmentedResultList, IPromiseReturnArgs } from '../../../src/components/AugmentedResultList/AugmentedResultList'; +import { AugmentedResultList, IPromiseReturnArgs, IAugmentData } from '../../../src/components/AugmentedResultList/AugmentedResultList'; describe('AugmentedResultList', () => { let sandbox: sinon.SinonSandbox; @@ -10,7 +10,7 @@ describe('AugmentedResultList', () => { let element: HTMLElement; let testOptions: Mock.AdvancedComponentSetupOptions; - const objectData = [ + const resultData = [ { id: '#001', name: 'bulbasaur', @@ -33,57 +33,61 @@ describe('AugmentedResultList', () => { starter: true, }; - const returnData: IPromiseReturnArgs = { + const returnData: IPromiseReturnArgs = { data: { - objectData, + resultData, commonData, }, }; const matchingIdField = '@id'; - const stubObjectAction = (objectIds: string[]): Promise => { + const stubFetchAugmentData = (objectIds: string[]): Promise> => { return Promise.resolve(returnData); }; + const failingFetchStub = (objectIds: string[]): Promise> => { + return Promise.reject('purposeful failure'); + }; + const createFakeResultsThatMatch = (numResults: number) => { const fakeResults = Fake.createFakeResults(numResults); fakeResults.results.forEach((result, index) => (result.raw.id = `#00${index}`)); return fakeResults; }; - const createComponent = (objectDataAction?: (objectIds: string[]) => Promise) => { + const createComponent = (fetchAugmentData?: (objectIds: string[]) => Promise>) => { element = document.createElement('div'); document.body.append(element); testOptions = new Mock.AdvancedComponentSetupOptions(element, { matchingIdField, - objectDataAction, + fetchAugmentData, }); componentSetup = Mock.advancedComponentSetup(AugmentedResultList, testOptions); return componentSetup; }; - const verifyAugmentedObjectData = (objectData: any, results: IQueryResult[]) => { - objectData.forEach((object: any) => { - const matchingResult = results.find((result) => result.raw.id === (object as any).id); + const verifyAugmentedResultData = (resultData: any, results: IQueryResult[]) => { + resultData.forEach((data: any) => { + const matchingResult = results.find((result) => result.raw.id === data.id); if (matchingResult) { - for (const key in object) { - expect(matchingResult.raw[key]).toEqual(object[key]); + for (const key in data) { + expect(matchingResult.raw[key]).toEqual(data[key]); } } }); }; - const verifyUntouchedResults = (objectData: {}[], results: IQueryResult[]) => { + const verifyUntouchedResults = (resultData: {}[], results: IQueryResult[]) => { const idString = matchingIdField.replace('@', ''); - objectData.forEach((object: any) => { - const ids = objectData.map((data: any) => (data as any)[idString]); + resultData.forEach((data: any) => { + const ids = resultData.map((data: any) => (data as any)[idString]); const otherResults = results.filter((result) => !ids.find((id) => (result as any)[idString] === id)); otherResults.forEach((result: IQueryResult) => { - for (const key in object) { + for (const key in data) { if (key !== idString) { expect(result.raw[key]).toBeUndefined; } @@ -100,10 +104,10 @@ describe('AugmentedResultList', () => { }); }; - const verifyAugmentedResults = (returnData: IPromiseReturnArgs, results: IQueryResult[]) => { - verifyAugmentedObjectData(returnData.data.objectData, results); + const verifyAugmentedResults = (returnData: IPromiseReturnArgs, results: IQueryResult[]) => { + verifyAugmentedResultData(returnData.data.resultData, results); verifyAugmentedCommonData(returnData.data.commonData, results); - verifyUntouchedResults(returnData.data.objectData, results); + verifyUntouchedResults(returnData.data.resultData, results); }; beforeAll(() => { @@ -127,7 +131,7 @@ describe('AugmentedResultList', () => { it('should augment results with object data', (done) => { const numResults = 10; const data = createFakeResultsThatMatch(numResults); - const test = createComponent(stubObjectAction); + const test = createComponent(stubFetchAugmentData); test.cmp.buildResults(data).then(() => { expect(test.cmp.getDisplayedResults().length).toEqual(numResults); @@ -139,7 +143,7 @@ describe('AugmentedResultList', () => { it("should NOT augment results if IDs don't match", (done) => { const numResults = 10; const data = Fake.createFakeResults(numResults); - const test = createComponent(stubObjectAction); + const test = createComponent(stubFetchAugmentData); test.cmp.buildResults(data).then(() => { expect(test.cmp.getDisplayedResults().length).toEqual(numResults); @@ -148,7 +152,7 @@ describe('AugmentedResultList', () => { }); }); - it('should still build results without augmenting if objectDataAction is missing', (done) => { + it('should still build results without augmenting if resultDataAction is missing', (done) => { const numResults = 10; const data = createFakeResultsThatMatch(numResults); const test = createComponent(); @@ -159,4 +163,16 @@ describe('AugmentedResultList', () => { done(); }); }); + + it('should fail gracefully and log an error', (done) => { + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(failingFetchStub); + const loggerSpy = sandbox.spy(Logger.prototype, 'error'); + + test.cmp.buildResults(data).then(() => { + expect(loggerSpy.calledWith(['Unable to fetch augment data.', 'purposeful failure'])).toBeTrue(); + done(); + }); + }); }); From bfe1c8c4f1ab3806cdafd711f673ecdfa06e5729 Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Mon, 16 Nov 2020 10:33:20 -0500 Subject: [PATCH 4/8] feat(SFINT-3521): test diff --- .../AugmentedResultList/AugmentedResultList.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index 1732bfef..0afd6320 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -171,8 +171,8 @@ describe('AugmentedResultList', () => { const loggerSpy = sandbox.spy(Logger.prototype, 'error'); test.cmp.buildResults(data).then(() => { - expect(loggerSpy.calledWith(['Unable to fetch augment data.', 'purposeful failure'])).toBeTrue(); - done(); + expect(loggerSpy.calledWith(['Unable to fetch augment data.', 'purposeful failure'])).toBeTrue(); + done(); }); }); }); From f4e5f7a6aec67e9682537b8cc97166c97d8b6ea7 Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Mon, 16 Nov 2020 17:44:22 -0500 Subject: [PATCH 5/8] feat(SFINT-3521): added matching function option --- .../AugmentedResultList.ts | 28 +++++++++++++------ .../AugmentedResultList.spec.ts | 22 ++++++++++++++- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts index b797fb50..b3070e3b 100644 --- a/src/components/AugmentedResultList/AugmentedResultList.ts +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -10,11 +10,11 @@ export interface IAugmentData { /** * Data specific to a result with matching object id. */ - resultData: any[]; + resultData: {}[]; /** * Data to add to every result with matching object id. */ - commonData: any; + commonData: {}; } /** @@ -31,6 +31,7 @@ export interface IPromiseReturnArgs { export interface AugmentedResultListOptions extends IResultListOptions { matchingIdField: IFieldOption; fetchAugmentData: (objectIds: String[]) => Promise>; + matchingFunction: (augmentData: any, queryResult: IQueryResult) => boolean; } export class AugmentedResultList extends Coveo.ResultList implements IComponentBindings { @@ -52,6 +53,12 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB fetchAugmentData: ComponentOptions.buildCustomOption<(objectIds: String[]) => Promise>>(() => { return null; }), + /** + * The function to use to determine a match between augment data and query results. + */ + matchingFunction: ComponentOptions.buildCustomOption<(augmentData: any, queryResult: IQueryResult) => boolean>(() => { + return null; + }), }; /** @@ -63,8 +70,16 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB */ constructor(public element: HTMLElement, public options: AugmentedResultListOptions, public bindings: IComponentBindings) { super(element, ComponentOptions.initComponentOptions(element, AugmentedResultList, options), bindings, AugmentedResultList.ID); + if (!this.options.matchingFunction) { + this.options.matchingFunction = this.defaultMatchingFunction; + } } + private defaultMatchingFunction = (augmentData: any, queryResult: IQueryResult) => { + const fieldName = this.getFieldString(this.options.matchingIdField); + return augmentData[fieldName] === queryResult.raw[fieldName]; + }; + private getObjectPayload(results: IQueryResult[]): String[] { const field = this.getFieldString(this.options.matchingIdField); if (results.length > 0) { @@ -83,10 +98,9 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB } public async buildResults(queryResults: Coveo.IQueryResults): Promise { - const fieldString = this.getFieldString(this.options.matchingIdField); + let remoteResults: IPromiseReturnArgs; if (this.options.fetchAugmentData) { - let remoteResults: IPromiseReturnArgs; try { // Call remote action to fetch augmenting data remoteResults = await this.options.fetchAugmentData(this.getObjectPayload(queryResults.results)); @@ -96,11 +110,9 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB } if (remoteResults?.data) { - // Merge remote action results with Coveo Results + // Merge augmenting data with Coveo Results queryResults.results.forEach((res: Coveo.IQueryResult) => { - const match = remoteResults.data.resultData.find((data) => { - return (data as any)[fieldString] === res.raw[fieldString]; - }); + const match = remoteResults.data.resultData.find((data: any) => this.options.matchingFunction(data, res)); // Attach data specific to each result/object for (const key in match) { diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index 0afd6320..3c4686b7 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -50,18 +50,23 @@ describe('AugmentedResultList', () => { return Promise.reject('purposeful failure'); }; + const testMatchingFunction = (augmentData: any, queryResult: IQueryResult) => { + return augmentData.type === 'water' && (augmentData.id === queryResult.raw.id); + } + const createFakeResultsThatMatch = (numResults: number) => { const fakeResults = Fake.createFakeResults(numResults); fakeResults.results.forEach((result, index) => (result.raw.id = `#00${index}`)); return fakeResults; }; - const createComponent = (fetchAugmentData?: (objectIds: string[]) => Promise>) => { + const createComponent = (fetchAugmentData?: (objectIds: string[]) => Promise>, matchingFunction?: (augmentData: any, queryResult: IQueryResult) => boolean) => { element = document.createElement('div'); document.body.append(element); testOptions = new Mock.AdvancedComponentSetupOptions(element, { matchingIdField, fetchAugmentData, + matchingFunction, }); componentSetup = Mock.advancedComponentSetup(AugmentedResultList, testOptions); @@ -140,6 +145,21 @@ describe('AugmentedResultList', () => { }); }); + it('should augment results with object data using provided matching function', (done) => { + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(stubFetchAugmentData, testMatchingFunction); + + test.cmp.buildResults(data).then(() => { + const displayedResults = test.cmp.getDisplayedResults(); + expect(displayedResults.length).toEqual(numResults); + expect(displayedResults.find(result => result.raw.id === '#007').raw.name).toEqual('squirtle'); + expect(displayedResults.find(result => result.raw.id === '#001').raw.name).toBeUndefined(); + expect(displayedResults.find(result => result.raw.id === '#004').raw.name).toBeUndefined(); + done(); + }); + }); + it("should NOT augment results if IDs don't match", (done) => { const numResults = 10; const data = Fake.createFakeResults(numResults); From 936ef16a51297018bde8a22ea04ae587c53a25ae Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Mon, 16 Nov 2020 17:44:42 -0500 Subject: [PATCH 6/8] feat(SFINT-3521): lint --- .../AugmentedResultList.spec.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index 3c4686b7..2b274df3 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -51,8 +51,8 @@ describe('AugmentedResultList', () => { }; const testMatchingFunction = (augmentData: any, queryResult: IQueryResult) => { - return augmentData.type === 'water' && (augmentData.id === queryResult.raw.id); - } + return augmentData.type === 'water' && augmentData.id === queryResult.raw.id; + }; const createFakeResultsThatMatch = (numResults: number) => { const fakeResults = Fake.createFakeResults(numResults); @@ -60,7 +60,10 @@ describe('AugmentedResultList', () => { return fakeResults; }; - const createComponent = (fetchAugmentData?: (objectIds: string[]) => Promise>, matchingFunction?: (augmentData: any, queryResult: IQueryResult) => boolean) => { + const createComponent = ( + fetchAugmentData?: (objectIds: string[]) => Promise>, + matchingFunction?: (augmentData: any, queryResult: IQueryResult) => boolean + ) => { element = document.createElement('div'); document.body.append(element); testOptions = new Mock.AdvancedComponentSetupOptions(element, { @@ -153,9 +156,9 @@ describe('AugmentedResultList', () => { test.cmp.buildResults(data).then(() => { const displayedResults = test.cmp.getDisplayedResults(); expect(displayedResults.length).toEqual(numResults); - expect(displayedResults.find(result => result.raw.id === '#007').raw.name).toEqual('squirtle'); - expect(displayedResults.find(result => result.raw.id === '#001').raw.name).toBeUndefined(); - expect(displayedResults.find(result => result.raw.id === '#004').raw.name).toBeUndefined(); + expect(displayedResults.find((result) => result.raw.id === '#007').raw.name).toEqual('squirtle'); + expect(displayedResults.find((result) => result.raw.id === '#001').raw.name).toBeUndefined(); + expect(displayedResults.find((result) => result.raw.id === '#004').raw.name).toBeUndefined(); done(); }); }); From 1480a461e1d9a79c15885699ce166adb90528efc Mon Sep 17 00:00:00 2001 From: Nathan Lafrance-Berger Date: Tue, 17 Nov 2020 09:42:18 -0500 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: Jeremie Robert <43446516+jeremierobert-coveo@users.noreply.github.com> --- .../AugmentedResultList/AugmentedResultList.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts index b3070e3b..cda9a9bb 100644 --- a/src/components/AugmentedResultList/AugmentedResultList.ts +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -70,9 +70,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB */ constructor(public element: HTMLElement, public options: AugmentedResultListOptions, public bindings: IComponentBindings) { super(element, ComponentOptions.initComponentOptions(element, AugmentedResultList, options), bindings, AugmentedResultList.ID); - if (!this.options.matchingFunction) { - this.options.matchingFunction = this.defaultMatchingFunction; - } + this.options.matchingFunction = this.options.matchingFunction ?? this.defaultMatchingFunction; } private defaultMatchingFunction = (augmentData: any, queryResult: IQueryResult) => { @@ -82,10 +80,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB private getObjectPayload(results: IQueryResult[]): String[] { const field = this.getFieldString(this.options.matchingIdField); - if (results.length > 0) { - return results.filter((result) => result.raw && result.raw[field]).map((result) => result.raw[field]); - } - return []; + return results.filter((result) => result.raw && result.raw[field]).map((result) => result.raw[field]); } private getFieldString(fieldName: IFieldOption) { @@ -93,8 +88,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB } public renderResults(resultElements: HTMLElement[], append = false): Promise { - const res = super.renderResults(resultElements, append); - return res; + return super.renderResults(resultElements, append); } public async buildResults(queryResults: Coveo.IQueryResults): Promise { @@ -132,8 +126,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB this.logger.error('No objectDataAction is defined.'); } - const ret = super.buildResults(queryResults); - return ret; + return super.buildResults(queryResults); } } From b1c6f4baab3debb4b76ddf9ded4d6a215342ce8b Mon Sep 17 00:00:00 2001 From: nlafrance-coveo Date: Tue, 17 Nov 2020 10:12:05 -0500 Subject: [PATCH 8/8] feat(SFINT-3521): added overwrite test --- .../AugmentedResultList/AugmentedResultList.ts | 15 ++++++++------- .../AugmentedResultList.spec.ts | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/components/AugmentedResultList/AugmentedResultList.ts b/src/components/AugmentedResultList/AugmentedResultList.ts index b3070e3b..10c874aa 100644 --- a/src/components/AugmentedResultList/AugmentedResultList.ts +++ b/src/components/AugmentedResultList/AugmentedResultList.ts @@ -76,20 +76,20 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB } private defaultMatchingFunction = (augmentData: any, queryResult: IQueryResult) => { - const fieldName = this.getFieldString(this.options.matchingIdField); + const fieldName = this.getMatchingFieldString(); return augmentData[fieldName] === queryResult.raw[fieldName]; }; private getObjectPayload(results: IQueryResult[]): String[] { - const field = this.getFieldString(this.options.matchingIdField); + const field = this.getMatchingFieldString(); if (results.length > 0) { return results.filter((result) => result.raw && result.raw[field]).map((result) => result.raw[field]); } return []; } - private getFieldString(fieldName: IFieldOption) { - return fieldName.replace('@', ''); + private getMatchingFieldString() { + return this.options.matchingIdField.replace('@', ''); } public renderResults(resultElements: HTMLElement[], append = false): Promise { @@ -99,6 +99,7 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB public async buildResults(queryResults: Coveo.IQueryResults): Promise { let remoteResults: IPromiseReturnArgs; + const fieldName = this.getMatchingFieldString(); if (this.options.fetchAugmentData) { try { @@ -112,14 +113,14 @@ export class AugmentedResultList extends Coveo.ResultList implements IComponentB if (remoteResults?.data) { // Merge augmenting data with Coveo Results queryResults.results.forEach((res: Coveo.IQueryResult) => { - const match = remoteResults.data.resultData.find((data: any) => this.options.matchingFunction(data, res)); + const match: any = remoteResults.data.resultData.find((data: any) => this.options.matchingFunction(data, res)); // Attach data specific to each result/object for (const key in match) { - if (Boolean(res.raw[key.toLowerCase()])) { + if (key.toLowerCase() !== fieldName && Boolean(res.raw[key.toLowerCase()])) { this.logger.warn(`The ${key} field was overwritten on result: ${res.title}`); } - res.raw[key.toLowerCase()] = (match as any)[key]; + res.raw[key.toLowerCase()] = match[key]; } // Attach data common to all results diff --git a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts index 2b274df3..df364577 100644 --- a/tests/components/AugmentedResultList/AugmentedResultList.spec.ts +++ b/tests/components/AugmentedResultList/AugmentedResultList.spec.ts @@ -163,6 +163,24 @@ describe('AugmentedResultList', () => { }); }); + it('should augment results with object data and warn if an overwrite occured', (done) => { + const numResults = 10; + const data = createFakeResultsThatMatch(numResults); + const test = createComponent(stubFetchAugmentData); + const loggerSpy = sandbox.spy(Logger.prototype, 'warn'); + + // Set attribute to be overwritten. + const overwrittenResult = data.results.find((res) => res.raw.id === '#001'); + overwrittenResult.raw.name = 'Mewtwo'; + + test.cmp.buildResults(data).then(() => { + expect(test.cmp.getDisplayedResults().length).toEqual(numResults); + expect(loggerSpy.calledWith(`The name field was overwritten on result: ${overwrittenResult.title}`)).toBeTrue(); + verifyAugmentedResults(returnData, test.cmp.getDisplayedResults()); + done(); + }); + }); + it("should NOT augment results if IDs don't match", (done) => { const numResults = 10; const data = Fake.createFakeResults(numResults);