From 1a3803ecee5cad83847f37ba9debca0a667c18e9 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Fri, 30 Oct 2020 23:22:50 +0000 Subject: [PATCH 01/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6824571eca..1e9902e2d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.746", + "version": "2.0.747", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From a2f491cc7e736dd69623572cd9cdf35415811541 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sat, 31 Oct 2020 22:13:30 +0000 Subject: [PATCH 02/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e9902e2d7..7890930c7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.747", + "version": "2.0.748", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 8586c5132442670ffe198b6633e4244b429d7ba3 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sun, 1 Nov 2020 22:15:27 +0000 Subject: [PATCH 03/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7890930c7c..c249c4ed60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.748", + "version": "2.0.749", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 6eff507dcd8bbfb59cbf071e8020be9a5da34093 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Mon, 2 Nov 2020 23:04:28 +0000 Subject: [PATCH 04/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c249c4ed60..34005fb7ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.749", + "version": "2.0.750", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 9bdbeeae2c379c9a372b6eca0197e5090d5951e2 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Tue, 3 Nov 2020 23:06:05 +0000 Subject: [PATCH 05/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 34005fb7ed..acfa6ae2f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.750", + "version": "2.0.751", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 2eab26363080fa7dc43e2bed3ed975bbe1737065 Mon Sep 17 00:00:00 2001 From: btaillon <54454747+btaillon@users.noreply.github.com> Date: Wed, 4 Nov 2020 12:03:51 -0500 Subject: [PATCH 06/25] Updated access key id and secret for dev environment (#1649) https://coveord.atlassian.net/browse/JSUI-3147 --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4fa70ef181..aa39dd140b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -117,9 +117,10 @@ deploy: all_branches: true condition: "$HAS_RELEASE_TAG == true" - provider: s3 - access_key_id: AKIAQ6FOLK5RDONGHVBY + access_key_id: + secure: "QHenZfIwQHlgIAj0MVHmBk9hfAF5cYzvl9z2WiA2SC9YBbpfc5coyVeHwviE3gqb2kPSKujSZA7/GexmJuWvJMrOuuVTPtK9vJualXxntFK6pj3wLnxbsa3KEZu7ntcbuQwRy3i4QelTs5JswHJLLNsz/yh6JnI74j0DGNUu/oIBD/ZiWEqxQXwxme0/6MLZAidE4RvWvVfcME0JbIBCkUvUsJe3wZg/ftdGRyONImRn5w19hrkZHReFIA7uTjljRrxJlAvhJK1eKUJEEFe7N4LEzAYsO8i3yL5YhQWhuYYxDtOh+nq2Eb63VFMW0kFwSQB+Gu9YlA7x4vM+NxM6SRL2NdywILsmcoVpgJXXl4viOd3mIY1GK0ulafUx9Q3T+GTN/gJopziS3aTZntRq+6xdj5QR437f51W4Yjqjn8dBDopmv+jFx2flaHWPNcAee+YThK/p8WVqboRQqIDi50J8OslaDF1ALsygRQFEwPy6FK5U9z16KstN+DQ9QoAwBUIw9e5NWZE+pKA4SEsvlSFVGEeM+IiudaUoG/Bs4qgkmFpWvaCCk0Ki1dS4VdaTIp7M7hbY3Ad6Dy+aU279I5+iDUx1y2wMEknsbGzKNDFeimcdSxdLRElFoLP7l1UJjyH/wxazCxRfG8oCCQPFEivgLT7XYyf+4IHLnBhg3yw=" secret_access_key: - secure: XL74wJ5088h082Ev6ZwPdEInliIF7/Dqvqz4rJYaXS3CU/KpsoeDhtV4wnF7Dr6P61AF6x8B4Q4uJKnOYBpg7oBjY7efsCDTJo2FeGxqM7j58Trdmh+kfDLmo9Rt06mTyvhKA/DC9tb7/Didnqs4/zDW0NUTqWu5ggnu0GO54kqoZon776ZCVrLzvSPpDh1VLqNTK5Z2ozIlO3XJc3y+QZeeG/yJBb3RcOnhv91+EIHib5y+HU5SoBIq71m2dbrR3q+V80mX+b9RAJDyb6wRjF8lDnrybaMF6Vf+FfzDj3fu1Z+juro8LN8afwMwKCZ4t8UHEB5n5/eD3oD6NwIkOQEPWMshbJr3UdM524dFrtcKQ5F7nvY8ZCE1a6zMGofmzINOb+qpiR5hlvIP375TaTVt7wIe7MM1CIuBferIqjL9fPmaMqcNRAxDt1HpI9HKOJ5ETSVXEpxviaBqYJjACwXgaXU4+5GsmcSprBSaBuBGJTVWQ5N7ewsxRP/2c3XmtMkB9lc+exwpF8/FvsJ6RbzNi1RhbA78r67XJcuK/2xD4aVYByd6PETVM1I7Bq0+LrECeEDqsTMcGHfAwc6ir69Lw0t/KVG/yHwbWyfwiYbkajfcQWYU669rD6ikY1B87assrqPDXneu65FxmoCYDMwBGRp8/w2gS7HSZmom5zA= + secure: "DtMdT58NMDONuDTyoRUeROF8ZvrsumV8iEQhOJxaFDPPdDl4FTv2JtZEwn59ne9HyE8MlFEWn9of9xsblDw+iCF3nhA+9hqJ9PiMY06rbE+QX5v6lu72LJEBv5Mg4oOj4P5oFIoliEQzBRFnoPjxYC1ptduXF1KF3jpDB+lUrsy5Dli+4KVnnMfU+5lSEDu6hUxM9IJvYHL4ChB0jaPf57YTlThGTVzZCle5xYBcHz2QWo3lI3QBy6n6Hp10gDZl1MqrjafpeznrGB98BCri+Ld+iTpCtO2yG4Zh+7G/gr7iqw1jUq6NlalyHwHRIOAxMu6l5GuCcJ9GmyCeFLOtpK/eHbMySAQUC/coqghneNgO0PohkrY9gMw4m6bfUiT3woyMtQixm+FNR11lDbeuvPgfbcDpXN7phBOQt2OY3m3o9e1SznrDLtVD7W8CQWRWdoyQvikb52vtuljpDcq7SKERNGaGYziXUsPrEtAhRj7+K7VRH+OOZpsm7kdlcyG6zkmd5UeUxvmz7zW+Ff27NA1ElyCHNFrYgJoWD+ceL8wNyg8KBlubu33+FU04wagNCK/5OxSFgAVsPztTkWaIeAmoVK033QgSOho+LCEFIMe1e0+lV8b5lF+P3nh9KtJgZJqQ/W1SNhMPsZtUJX5wrPQnXNUVxRs/QubB3rz4KiI=" bucket: coveo-ndev-binaries local-dir: bin upload-dir: proda/StaticCDN/searchui/nightly @@ -131,9 +132,10 @@ deploy: all_branches: true condition: "$IS_NIGHTLY == true" - provider: s3 - access_key_id: AKIAQ6FOLK5RDONGHVBY + access_key_id: + secure: "QHenZfIwQHlgIAj0MVHmBk9hfAF5cYzvl9z2WiA2SC9YBbpfc5coyVeHwviE3gqb2kPSKujSZA7/GexmJuWvJMrOuuVTPtK9vJualXxntFK6pj3wLnxbsa3KEZu7ntcbuQwRy3i4QelTs5JswHJLLNsz/yh6JnI74j0DGNUu/oIBD/ZiWEqxQXwxme0/6MLZAidE4RvWvVfcME0JbIBCkUvUsJe3wZg/ftdGRyONImRn5w19hrkZHReFIA7uTjljRrxJlAvhJK1eKUJEEFe7N4LEzAYsO8i3yL5YhQWhuYYxDtOh+nq2Eb63VFMW0kFwSQB+Gu9YlA7x4vM+NxM6SRL2NdywILsmcoVpgJXXl4viOd3mIY1GK0ulafUx9Q3T+GTN/gJopziS3aTZntRq+6xdj5QR437f51W4Yjqjn8dBDopmv+jFx2flaHWPNcAee+YThK/p8WVqboRQqIDi50J8OslaDF1ALsygRQFEwPy6FK5U9z16KstN+DQ9QoAwBUIw9e5NWZE+pKA4SEsvlSFVGEeM+IiudaUoG/Bs4qgkmFpWvaCCk0Ki1dS4VdaTIp7M7hbY3Ad6Dy+aU279I5+iDUx1y2wMEknsbGzKNDFeimcdSxdLRElFoLP7l1UJjyH/wxazCxRfG8oCCQPFEivgLT7XYyf+4IHLnBhg3yw=" secret_access_key: - secure: XL74wJ5088h082Ev6ZwPdEInliIF7/Dqvqz4rJYaXS3CU/KpsoeDhtV4wnF7Dr6P61AF6x8B4Q4uJKnOYBpg7oBjY7efsCDTJo2FeGxqM7j58Trdmh+kfDLmo9Rt06mTyvhKA/DC9tb7/Didnqs4/zDW0NUTqWu5ggnu0GO54kqoZon776ZCVrLzvSPpDh1VLqNTK5Z2ozIlO3XJc3y+QZeeG/yJBb3RcOnhv91+EIHib5y+HU5SoBIq71m2dbrR3q+V80mX+b9RAJDyb6wRjF8lDnrybaMF6Vf+FfzDj3fu1Z+juro8LN8afwMwKCZ4t8UHEB5n5/eD3oD6NwIkOQEPWMshbJr3UdM524dFrtcKQ5F7nvY8ZCE1a6zMGofmzINOb+qpiR5hlvIP375TaTVt7wIe7MM1CIuBferIqjL9fPmaMqcNRAxDt1HpI9HKOJ5ETSVXEpxviaBqYJjACwXgaXU4+5GsmcSprBSaBuBGJTVWQ5N7ewsxRP/2c3XmtMkB9lc+exwpF8/FvsJ6RbzNi1RhbA78r67XJcuK/2xD4aVYByd6PETVM1I7Bq0+LrECeEDqsTMcGHfAwc6ir69Lw0t/KVG/yHwbWyfwiYbkajfcQWYU669rD6ikY1B87assrqPDXneu65FxmoCYDMwBGRp8/w2gS7HSZmom5zA= + secure: "DtMdT58NMDONuDTyoRUeROF8ZvrsumV8iEQhOJxaFDPPdDl4FTv2JtZEwn59ne9HyE8MlFEWn9of9xsblDw+iCF3nhA+9hqJ9PiMY06rbE+QX5v6lu72LJEBv5Mg4oOj4P5oFIoliEQzBRFnoPjxYC1ptduXF1KF3jpDB+lUrsy5Dli+4KVnnMfU+5lSEDu6hUxM9IJvYHL4ChB0jaPf57YTlThGTVzZCle5xYBcHz2QWo3lI3QBy6n6Hp10gDZl1MqrjafpeznrGB98BCri+Ld+iTpCtO2yG4Zh+7G/gr7iqw1jUq6NlalyHwHRIOAxMu6l5GuCcJ9GmyCeFLOtpK/eHbMySAQUC/coqghneNgO0PohkrY9gMw4m6bfUiT3woyMtQixm+FNR11lDbeuvPgfbcDpXN7phBOQt2OY3m3o9e1SznrDLtVD7W8CQWRWdoyQvikb52vtuljpDcq7SKERNGaGYziXUsPrEtAhRj7+K7VRH+OOZpsm7kdlcyG6zkmd5UeUxvmz7zW+Ff27NA1ElyCHNFrYgJoWD+ceL8wNyg8KBlubu33+FU04wagNCK/5OxSFgAVsPztTkWaIeAmoVK033QgSOho+LCEFIMe1e0+lV8b5lF+P3nh9KtJgZJqQ/W1SNhMPsZtUJX5wrPQnXNUVxRs/QubB3rz4KiI=" bucket: coveo-ndev-binaries local-dir: bin upload-dir: proda/StaticCDN/searchui/pr/$TRAVIS_BRANCH From cf8dacdcc20dede77180d6e53ebd7bbd81cd2dc7 Mon Sep 17 00:00:00 2001 From: Sami Date: Wed, 4 Nov 2020 14:41:46 -0500 Subject: [PATCH 07/25] JSUI-3145 Prevent starting href template with protocols that can be used for XSS (#1648) https://coveord.atlassian.net/browse/JSUI-3145 --- src/ui/ResultLink/ResultLink.ts | 3 ++- unitTests/ui/PrintableUriTest.ts | 26 +++++++++++++------------- unitTests/ui/ResultLinkTest.ts | 29 ++++++++++++++++++----------- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/ui/ResultLink/ResultLink.ts b/src/ui/ResultLink/ResultLink.ts index ef683d5aa0..e5f038fe63 100644 --- a/src/ui/ResultLink/ResultLink.ts +++ b/src/ui/ResultLink/ResultLink.ts @@ -487,7 +487,8 @@ export class ResultLink extends Component { private getResultUri(): string { if (this.options.hrefTemplate) { - return StringUtils.buildStringTemplateFromResult(this.options.hrefTemplate, this.result); + const uri = StringUtils.buildStringTemplateFromResult(this.options.hrefTemplate, this.result); + return this.filterProtocol(uri); } if (this.options.field == undefined && this.options.openInOutlook) { diff --git a/unitTests/ui/PrintableUriTest.ts b/unitTests/ui/PrintableUriTest.ts index 4d1173de5d..5d7c1fcd89 100644 --- a/unitTests/ui/PrintableUriTest.ts +++ b/unitTests/ui/PrintableUriTest.ts @@ -35,7 +35,7 @@ const longParents: IParent[] = range(longParentsCount).map(index => ({ name: ind const longParentsXml = generateParentsXMLFromList(longParents); export function PrintableUriTest() { - describe('PrintableUri', function() { + describe('PrintableUri', function () { let test: Mock.IBasicComponentSetup; let fakeResult: IQueryResult; @@ -56,7 +56,7 @@ export function PrintableUriTest() { spyOn(window, 'open'); }); - afterEach(function() { + afterEach(function () { test = null; fakeResult = null; }); @@ -243,7 +243,7 @@ export function PrintableUriTest() { describe('exposes hrefTemplate', () => { it('should not modify the href template if there are no field specified', () => { - let hrefTemplate = 'test'; + let hrefTemplate = 'http://test'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, @@ -254,61 +254,61 @@ export function PrintableUriTest() { }); it('should replace fields in the href template by the results equivalent', () => { - let hrefTemplate = '${summary}'; + let hrefTemplate = 'http://${summary}'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, fakeResult ); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.summary, jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.summary}`, jasmine.anything()); }); it('should support nested values in result', () => { - let hrefTemplate = '${raw.number}'; + let hrefTemplate = 'http://${raw.number}'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, fakeResult ); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.raw['number'].toString(), jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.raw['number'].toString()}`, jasmine.anything()); }); it('should not parse standalone accolades', () => { - let hrefTemplate = '${raw.number}{test}'; + let hrefTemplate = 'http://${raw.number}{test}'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, fakeResult ); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.raw['number'] + '{test}', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.raw['number']}{test}`, jasmine.anything()); }); it('should support external fields', () => { window['Coveo']['test'] = 'testExternal'; - let hrefTemplate = '${Coveo.test}'; + let hrefTemplate = 'http://${Coveo.test}'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, fakeResult ); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith('testExternal', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith('http://testExternal', jasmine.anything()); window['Coveo']['test'] = undefined; }); it('should support nested external fields with more than 2 keys', () => { window['Coveo']['test'] = { key: 'testExternal' }; - let hrefTemplate = '${Coveo.test.key}'; + let hrefTemplate = 'http://${Coveo.test.key}'; test = Mock.optionsResultComponentSetup( PrintableUri, { hrefTemplate: hrefTemplate }, fakeResult ); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith('testExternal', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith('http://testExternal', jasmine.anything()); window['Coveo']['test'] = undefined; }); }); diff --git a/unitTests/ui/ResultLinkTest.ts b/unitTests/ui/ResultLinkTest.ts index 4271f04fad..ea8f7ee369 100644 --- a/unitTests/ui/ResultLinkTest.ts +++ b/unitTests/ui/ResultLinkTest.ts @@ -126,49 +126,56 @@ export function ResultLinkTest() { }); describe('exposes hrefTemplate', () => { + it('when the href starts with a protocol that enables XSS, it returns an empty string', () => { + const hrefTemplate = 'javascript:alert(1)'; + test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); + test.cmp.openLinkInNewWindow(); + expect(window.open).toHaveBeenCalledWith('', jasmine.anything()); + }); + it('should not modify the href template if there are no field specified', () => { - let hrefTemplate = 'test'; + let hrefTemplate = 'http://test'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); expect(window.open).toHaveBeenCalledWith(hrefTemplate, jasmine.anything()); }); it('should replace fields in the href template by the results equivalent', () => { - let hrefTemplate = '${title}'; + let hrefTemplate = 'http://${title}'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.title, jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.title}`, jasmine.anything()); }); it('should support nested values in result', () => { - let hrefTemplate = '${raw.number}'; + let hrefTemplate = 'http://${raw.number}'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.raw['number'].toString(), jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.raw['number'].toString()}`, jasmine.anything()); }); it('should not parse standalone accolades', () => { - let hrefTemplate = '${raw.number}{test}'; + let hrefTemplate = 'http://${raw.number}{test}'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith(fakeResult.raw['number'] + '{test}', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith(`http://${fakeResult.raw['number']}{test}`, jasmine.anything()); }); it('should support external fields', () => { window['Coveo']['test'] = 'testExternal'; - let hrefTemplate = '${Coveo.test}'; + let hrefTemplate = 'http://${Coveo.test}'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith('testExternal', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith('http://testExternal', jasmine.anything()); window['Coveo']['test'] = undefined; }); it('should support nested external fields with more than 2 keys', () => { window['Coveo']['test'] = { key: 'testExternal' }; - let hrefTemplate = '${Coveo.test.key}'; + let hrefTemplate = 'http://${Coveo.test.key}'; test = Mock.optionsResultComponentSetup(ResultLink, { hrefTemplate: hrefTemplate }, fakeResult); test.cmp.openLinkInNewWindow(); - expect(window.open).toHaveBeenCalledWith('testExternal', jasmine.anything()); + expect(window.open).toHaveBeenCalledWith('http://testExternal', jasmine.anything()); window['Coveo']['test'] = undefined; }); }); From e310b8bcc56904b606b0e4f58ddf1ba4a09a8e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-F=C3=A9lix=20Thibodeau?= Date: Wed, 4 Nov 2020 15:00:15 -0500 Subject: [PATCH 08/25] Resize mobile device on orientationchange (#1647) https://coveord.atlassian.net/browse/JSUI-3142 --- src/ui/ResponsiveComponents/ResponsiveComponentsManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/ResponsiveComponents/ResponsiveComponentsManager.ts b/src/ui/ResponsiveComponents/ResponsiveComponentsManager.ts index e6dd304bd5..6114d74440 100644 --- a/src/ui/ResponsiveComponents/ResponsiveComponentsManager.ts +++ b/src/ui/ResponsiveComponents/ResponsiveComponentsManager.ts @@ -157,6 +157,8 @@ export class ResponsiveComponentsManager { // Since on a mobile device resizing the page is not something that should really happen, we disable it here. if (!DeviceUtils.isMobileDevice()) { window.addEventListener('resize', this.resizeListener); + } else { + window.addEventListener('orientationchange', this.resizeListener); } this.bindNukeEvents(); From c305bc28288e96cd2d4a2b9601eb73453494ecc8 Mon Sep 17 00:00:00 2001 From: jpmarceau <39384459+jpmarceau@users.noreply.github.com> Date: Wed, 4 Nov 2020 15:26:28 -0500 Subject: [PATCH 09/25] Doc update following data residency, and removing cloudplatform (#1644) https://coveord.atlassian.net/browse/JSUI-1065 --- src/rest/SearchEndpoint.ts | 3 ++- src/ui/Analytics/Analytics.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rest/SearchEndpoint.ts b/src/rest/SearchEndpoint.ts index 703ff45791..2b8a9bbf83 100644 --- a/src/rest/SearchEndpoint.ts +++ b/src/rest/SearchEndpoint.ts @@ -167,8 +167,9 @@ export class SearchEndpoint implements ISearchEndpoint { * **Allowed values:** * * - `https://platform.cloud.coveo.com/rest/search` (for organizations in the standard Coveo Cloud V2 environment) + * - `https://platform-eu.cloud.coveo.com/rest/search` (for organizations with European [data residency](https://docs.coveo.com/en/2976/#data-residency-configuration)) + * - `https://platform-au.cloud.coveo.com/rest/search` (for organizations with Australian data residency) * - `https://platformhipaa.cloud.coveo.com/rest/search` (for [HIPAA](https://docs.coveo.com/1853/) organizations) - * - `https://globalplatform.cloud.coveo.com/rest/search` (for [multi-region](https://docs.coveo.com/2976/) organizations) * * **Default:** `https://platform.cloud.coveo.com/rest/search` * @param otherOptions Additional options to apply for this endpoint (e.g., a [`renewAccessToken`]{@link ISearchEndpointOptions.renewAccessToken} function). diff --git a/src/ui/Analytics/Analytics.ts b/src/ui/Analytics/Analytics.ts index aa6a1282ca..a1daae575e 100644 --- a/src/ui/Analytics/Analytics.ts +++ b/src/ui/Analytics/Analytics.ts @@ -101,7 +101,10 @@ export class Analytics extends Component { * Specifies the URL of the Usage Analytics service. You do not have to specify a value for this option, unless * the location of the service you use differs from the default Coveo Cloud Usage Analytics endpoint. * - * Default value is `https://platform.cloud.coveo.com/rest/ua`. + * By deault, the value is `https://platform.cloud.coveo.com/rest/ua`, or + * `https://platform-.cloud.coveo.com/rest/ua` if you have + * [configured your search endpoint]{@link SearchEndpoint.configureCloudV2Endpoint} to implement + * data residency outside of the United States. */ endpoint: ComponentOptions.buildStringOption({ postProcessing: value => { From b2bffc6dd1ba3445ee4cdbe12e2c9de8f2fb8350 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Wed, 4 Nov 2020 22:51:14 +0000 Subject: [PATCH 10/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index acfa6ae2f5..58985a4ddd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.751", + "version": "2.0.752", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From ae2747fd2c3e9b39eb48fb3d2cdca5f682c900e7 Mon Sep 17 00:00:00 2001 From: btaillon <54454747+btaillon@users.noreply.github.com> Date: Thu, 5 Nov 2020 10:27:12 -0500 Subject: [PATCH 11/25] JSUI-3037 Improved analytics for search queries (#1643) Improved analytics for search queries * Introduced the analytics section * Introduced clientId * Introduced pageId https://coveord.atlassian.net/browse/JSUI-3037 --- deploy.beta.js | 4 +- karma.accessibility.test.conf.js | 2 +- karma.unit.test.conf.js | 2 +- package.json | 2 +- src/rest/AnalyticsEndpoint.ts | 4 +- src/rest/AnalyticsEvent.ts | 4 + src/rest/QuerySuggest.ts | 1 + src/rest/SearchEndpoint.ts | 139 ++++++++------------- src/ui/Analytics/Analytics.ts | 13 +- src/ui/Analytics/AnalyticsInformation.ts | 37 ++++++ src/ui/Analytics/LiveAnalyticsClient.ts | 4 +- src/ui/Base/RegisteredNamedMethods.ts | 18 ++- src/ui/CardActionBar/CardActionBar.ts | 6 +- src/ui/Quickview/Quickview.ts | 7 +- unitTests/Fake.ts | 6 +- unitTests/MockCookie.ts | 52 ++++++++ unitTests/Test.ts | 6 + unitTests/rest/EndpointCallerTest.ts | 48 +++---- unitTests/rest/SearchEndpointTest.ts | 71 ++++++++++- unitTests/ui/AnalyticsInformationTest.ts | 93 ++++++++++++++ unitTests/ui/CardActionBarTest.ts | 34 ++--- unitTests/ui/CheckboxTest.ts | 12 +- unitTests/ui/ExplanationModalTest.ts | 6 +- unitTests/ui/ExportToExcelTest.ts | 8 +- unitTests/ui/FacetTest.ts | 4 +- unitTests/ui/RegisteredNamedMethodsTest.ts | 12 +- unitTests/ui/SearchInterfaceTest.ts | 4 +- unitTests/utils/CookieUtilsTest.ts | 22 +--- unitTests/utils/DependsOnManagerTest.ts | 2 +- 29 files changed, 421 insertions(+), 202 deletions(-) create mode 100644 src/ui/Analytics/AnalyticsInformation.ts create mode 100644 unitTests/MockCookie.ts create mode 100644 unitTests/ui/AnalyticsInformationTest.ts diff --git a/deploy.beta.js b/deploy.beta.js index de6273382f..bdd8644eb5 100644 --- a/deploy.beta.js +++ b/deploy.beta.js @@ -19,8 +19,8 @@ if (travisBranchName) { } console.log('executing deploy script beta'); -setTimeout(function() { - exec(`npm publish --tag beta${branchToTag}`, function(error, stdout, stderr) { +setTimeout(function () { + exec(`npm publish --tag beta${branchToTag}`, function (error, stdout, stderr) { if (error) { console.log(error, stdout, stderr); process.exit(1); diff --git a/karma.accessibility.test.conf.js b/karma.accessibility.test.conf.js index 1d83192b3e..2690cd7549 100644 --- a/karma.accessibility.test.conf.js +++ b/karma.accessibility.test.conf.js @@ -37,6 +37,6 @@ const configuration = { } }; -module.exports = function(config) { +module.exports = function (config) { config.set(configuration); }; diff --git a/karma.unit.test.conf.js b/karma.unit.test.conf.js index ec5d019832..52e274e399 100644 --- a/karma.unit.test.conf.js +++ b/karma.unit.test.conf.js @@ -47,6 +47,6 @@ const configuration = { } }; -module.exports = function(config) { +module.exports = function (config) { config.set(configuration); }; diff --git a/package.json b/package.json index 58985a4ddd..a71948ae33 100644 --- a/package.json +++ b/package.json @@ -181,4 +181,4 @@ "@types/node": "~10.5.4" }, "snyk": true -} \ No newline at end of file +} diff --git a/src/rest/AnalyticsEndpoint.ts b/src/rest/AnalyticsEndpoint.ts index ed2ef41b81..603a70c51c 100644 --- a/src/rest/AnalyticsEndpoint.ts +++ b/src/rest/AnalyticsEndpoint.ts @@ -16,6 +16,7 @@ import { IAPIAnalyticsVisitResponseRest } from './APIAnalyticsVisitResponse'; import { ICustomEvent } from './CustomEvent'; import { ITopQueries } from './TopQueries'; import { SearchEndpoint } from '../rest/SearchEndpoint'; +import { AnalyticsInformation } from '../ui/Analytics/AnalyticsInformation'; export interface IAnalyticsEndpointOptions { accessToken: AccessToken; @@ -104,8 +105,7 @@ export class AnalyticsEndpoint { } public clearCookies() { - Cookie.erase('visitorId'); - Cookie.erase('visitId'); + new AnalyticsInformation().clearCookies(); } private async sendToService(data: Record, path: string, paramName: string): Promise { diff --git a/src/rest/AnalyticsEvent.ts b/src/rest/AnalyticsEvent.ts index 7acc167274..09fda85e2b 100644 --- a/src/rest/AnalyticsEvent.ts +++ b/src/rest/AnalyticsEvent.ts @@ -125,4 +125,8 @@ export interface IAnalyticsEvent { * **Example:** `{ "currentResultsPerPage": 25, "userRole": "developer" }` */ customData?: { [key: string]: any }; + /** + * A GUID representing the current user. This GUID is generated locally and stored in a non-expiring browser cookie. + */ + clientId: string; } diff --git a/src/rest/QuerySuggest.ts b/src/rest/QuerySuggest.ts index 3474b88fa2..864e7a0fbd 100644 --- a/src/rest/QuerySuggest.ts +++ b/src/rest/QuerySuggest.ts @@ -98,6 +98,7 @@ export interface IQuerySuggestRequest { /** * A GUID representing the current user, who can be authenticated or anonymous. This GUID is normally generated by the usage analytics service and stored in a non-expiring browser cookie. + * @deprecated */ visitorId?: string; diff --git a/src/rest/SearchEndpoint.ts b/src/rest/SearchEndpoint.ts index 2b8a9bbf83..80b662ff86 100644 --- a/src/rest/SearchEndpoint.ts +++ b/src/rest/SearchEndpoint.ts @@ -32,7 +32,6 @@ import { QueryError } from '../rest/QueryError'; import { Utils } from '../utils/Utils'; import * as _ from 'underscore'; import { buildHistoryStore } from '../utils/HistoryStore'; -import { Cookie } from '../utils/CookieUtils'; import { TimeSpan } from '../utils/TimeSpanUtils'; import { UrlUtils } from '../utils/UrlUtils'; import { IGroupByResult } from './GroupByResult'; @@ -41,6 +40,8 @@ import { BackOffRequest, IBackOffRequest } from './BackOffRequest'; import { IFacetSearchRequest } from './Facet/FacetSearchRequest'; import { IFacetSearchResponse } from './Facet/FacetSearchResponse'; import { IPlanResponse, ExecutionPlan } from './Plan'; +import { mapObject } from 'underscore'; +import { AnalyticsInformation } from '../ui/Analytics/AnalyticsInformation'; export class DefaultSearchEndpointOptions implements ISearchEndpointOptions { restUri: string; @@ -333,6 +334,7 @@ export class SearchEndpoint implements ISearchEndpoint { @includeActionsHistory() @includeReferrer() + @includeAnalytics() @includeVisitorId() @includeIsGuestUser() private buildCompleteCall(request: any, callOptions?: IEndpointCallOptions, callParams?: IEndpointCallParameters) { @@ -877,10 +879,6 @@ export class SearchEndpoint implements ISearchEndpoint { @method('POST') @requestDataType('application/json') @responseType('text') - @includeActionsHistory() - @includeReferrer() - @includeVisitorId() - @includeIsGuestUser() public async facetSearch( request: IFacetSearchRequest, callOptions?: IEndpointCallOptions, @@ -1082,7 +1080,7 @@ export class SearchEndpoint implements ISearchEndpoint { this.isRedirecting = true; } - private buildBaseUri(path: string): string { + public buildBaseUri(path: string): string { Assert.isString(path); return UrlUtils.normalizeAsString({ @@ -1276,18 +1274,19 @@ function defaultDecoratorEndpointCallParameters() { return params; } +function getEndpointCallParameters(nbParams: number, args: any[]) { + if (!args[nbParams - 1]) { + args[nbParams - 1] = defaultDecoratorEndpointCallParameters(); + } + return args[nbParams - 1] as IEndpointCallParameters; +} + function path(path: string) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - const url = this.buildBaseUri(path); - if (args[nbParams - 1]) { - args[nbParams - 1].url = url; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { url: url }); - args[nbParams - 1] = endpointCallParams; - } + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + getEndpointCallParameters(nbParams, args).url = this.buildBaseUri(path); return originalMethod.apply(this, args); }; @@ -1299,14 +1298,8 @@ function alertsPath(path: string) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - const url = this.buildSearchAlertsUri(path); - if (args[nbParams - 1]) { - args[nbParams - 1].url = url; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { url: url }); - args[nbParams - 1] = endpointCallParams; - } + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + getEndpointCallParameters(nbParams, args).url = this.buildSearchAlertsUri(path); return originalMethod.apply(this, args); }; @@ -1318,13 +1311,8 @@ function requestDataType(type: string) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - if (args[nbParams - 1]) { - args[nbParams - 1].requestDataType = type; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { requestDataType: type }); - args[nbParams - 1] = endpointCallParams; - } + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + getEndpointCallParameters(nbParams, args).requestDataType = type; return originalMethod.apply(this, args); }; return descriptor; @@ -1335,13 +1323,8 @@ function method(met: string) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - if (args[nbParams - 1]) { - args[nbParams - 1].method = met; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { method: met }); - args[nbParams - 1] = endpointCallParams; - } + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + getEndpointCallParameters(nbParams, args).method = met; return originalMethod.apply(this, args); }; @@ -1353,13 +1336,8 @@ function responseType(resp: string) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - if (args[nbParams - 1]) { - args[nbParams - 1].responseType = resp; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { responseType: resp }); - args[nbParams - 1] = endpointCallParams; - } + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + getEndpointCallParameters(nbParams, args).responseType = resp; return originalMethod.apply(this, args); }; @@ -1380,14 +1358,10 @@ function accessTokenInUrl(tokenKey: string = 'access_token') { return queryString; }; - descriptor.value = function (...args: any[]) { + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { const queryString = buildAccessToken(tokenKey, this); - if (args[nbParams - 1]) { - args[nbParams - 1].queryString = args[nbParams - 1].queryString.concat(queryString); - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { queryString: queryString }); - args[nbParams - 1] = endpointCallParams; - } + const params = getEndpointCallParameters(nbParams, args); + params.queryString = [...(params.queryString || []), ...queryString]; return originalMethod.apply(this, args); }; @@ -1399,20 +1373,13 @@ function includeActionsHistory(historyStore = buildHistoryStore()) { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { let historyFromStore = historyStore.getHistory(); if (historyFromStore == null) { historyFromStore = []; } - if (args[nbParams - 1]) { - args[nbParams - 1].requestData.actionsHistory = historyFromStore; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { - requestData: { actionsHistory: historyFromStore } - }); - args[nbParams - 1] = endpointCallParams; - } + getEndpointCallParameters(nbParams, args).requestData.actionsHistory = historyFromStore; return originalMethod.apply(this, args); }; @@ -1423,20 +1390,32 @@ function includeActionsHistory(historyStore = buildHistoryStore()) { function includeReferrer() { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { let referrer = document.referrer; if (referrer == null) { referrer = ''; } - if (args[nbParams - 1]) { - args[nbParams - 1].requestData.referrer = referrer; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { - requestData: { referrer: referrer } - }); - args[nbParams - 1] = endpointCallParams; - } + getEndpointCallParameters(nbParams, args).requestData.referrer = referrer; + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} + +function includeAnalytics() { + return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { + const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + const analyticsInstance = new AnalyticsInformation(); + const analytics = { + clientId: analyticsInstance.clientId, + documentLocation: analyticsInstance.location, + documentReferrer: analyticsInstance.referrer, + pageId: analyticsInstance.lastPageId + }; + getEndpointCallParameters(nbParams, args).requestData.analytics = mapObject(analytics, value => value || ''); return originalMethod.apply(this, args); }; @@ -1447,20 +1426,13 @@ function includeReferrer() { function includeVisitorId() { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { - let visitorId = Cookie.get('visitorId'); + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { + let visitorId = new AnalyticsInformation().visitorId; if (visitorId == null) { visitorId = ''; } - if (args[nbParams - 1]) { - args[nbParams - 1].requestData.visitorId = visitorId; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { - requestData: { visitorId: visitorId } - }); - args[nbParams - 1] = endpointCallParams; - } + getEndpointCallParameters(nbParams, args).requestData.visitorId = visitorId; return originalMethod.apply(this, args); }; @@ -1471,17 +1443,10 @@ function includeVisitorId() { function includeIsGuestUser() { return function (target: Object, key: string, descriptor: TypedPropertyDescriptor) { const { originalMethod, nbParams } = decoratorSetup(target, key, descriptor); - descriptor.value = function (...args: any[]) { + descriptor.value = function (this: SearchEndpoint, ...args: any[]) { let isGuestUser = this.options.isGuestUser; - if (args[nbParams - 1]) { - args[nbParams - 1].requestData.isGuestUser = isGuestUser; - } else { - const endpointCallParams = _.extend(defaultDecoratorEndpointCallParameters(), { - requestData: { isGuestUser: isGuestUser } - }); - args[nbParams - 1] = endpointCallParams; - } + getEndpointCallParameters(nbParams, args).requestData.isGuestUser = isGuestUser; return originalMethod.apply(this, args); }; diff --git a/src/ui/Analytics/Analytics.ts b/src/ui/Analytics/Analytics.ts index a1daae575e..f5be93ab9c 100644 --- a/src/ui/Analytics/Analytics.ts +++ b/src/ui/Analytics/Analytics.ts @@ -23,6 +23,9 @@ import { PendingSearchEvent } from './PendingSearchEvent'; import { PendingSearchAsYouTypeSearchEvent } from './PendingSearchAsYouTypeSearchEvent'; import { AccessToken } from '../../rest/AccessToken'; import { AnalyticsEvents, IAnalyticsEventArgs } from '../../events/AnalyticsEvents'; +import { Cookie } from '../../utils/CookieUtils'; +import { QueryUtils } from '../../utils/QueryUtils'; +import { AnalyticsInformation } from './AnalyticsInformation'; export interface IAnalyticsOptions { user?: string; @@ -257,6 +260,8 @@ export class Analytics extends Component { let event = this.componentOptionsModel.getEventName(Model.eventTypes.changeOne + ComponentOptionsModel.attributesEnum.searchHub); this.bind.onRootElement(event, (args: IAttributeChangedEventArg) => this.handleSearchHubChanged(args)); } + + this.createClientId(); } /** @@ -408,7 +413,7 @@ export class Analytics extends Component { if (this.disabled || this.client instanceof NoopAnalyticsClient) { return this.logger.warn('Could not clear local data while analytics are disabled.'); } - this.client.endpoint.clearCookies(); + new AnalyticsInformation().clearCookies(); this.resolveQueryController().resetHistory(); } @@ -458,6 +463,12 @@ export class Analytics extends Component { }); } + private createClientId() { + if (!new AnalyticsInformation().clientId) { + Cookie.set('clientId', QueryUtils.createGuid()); + } + } + private initializeAnalyticsClient() { if (Utils.isNonEmptyString(this.options.endpoint)) { let endpoint = this.initializeAnalyticsEndpoint(); diff --git a/src/ui/Analytics/AnalyticsInformation.ts b/src/ui/Analytics/AnalyticsInformation.ts new file mode 100644 index 0000000000..29acb6d621 --- /dev/null +++ b/src/ui/Analytics/AnalyticsInformation.ts @@ -0,0 +1,37 @@ +import { history } from 'coveo.analytics'; +import { findLastIndex } from 'underscore'; +import { Cookie } from '../../utils/CookieUtils'; + +export class AnalyticsInformation { + public get visitorId() { + return Cookie.get('visitorId') || null; + } + + public get clientId() { + return Cookie.get('clientId') || null; + } + + public get lastPageId() { + const store = new history.HistoryStore(); + const actions = store.getHistory() as { name: string; value?: string }[]; + const pageViewActionId = findLastIndex(actions, action => action.name === 'PageView'); + if (pageViewActionId === -1) { + return null; + } + return actions[pageViewActionId].value; + } + + public get location() { + return document.location.href; + } + + public get referrer() { + return document.referrer; + } + + public clearCookies() { + Cookie.erase('visitorId'); + Cookie.erase('clientId'); + Cookie.erase('visitId'); + } +} diff --git a/src/ui/Analytics/LiveAnalyticsClient.ts b/src/ui/Analytics/LiveAnalyticsClient.ts index ff5e82ee46..285414320f 100644 --- a/src/ui/Analytics/LiveAnalyticsClient.ts +++ b/src/ui/Analytics/LiveAnalyticsClient.ts @@ -29,6 +29,7 @@ import { version } from '../../misc/Version'; import { QueryUtils } from '../../utils/QueryUtils'; import * as _ from 'underscore'; import { IComponentBindings } from '../Base/ComponentBindings'; +import { AnalyticsInformation } from './AnalyticsInformation'; export class LiveAnalyticsClient implements IAnalyticsClient { public isContextual: boolean = false; @@ -275,7 +276,8 @@ export class LiveAnalyticsClient implements IAnalyticsClient { originLevel3: document.referrer, originContext: this.originContext, customData: _.keys(metaObject).length > 0 ? metaObject : undefined, - userAgent: navigator.userAgent + userAgent: navigator.userAgent, + clientId: new AnalyticsInformation().clientId }; } diff --git a/src/ui/Base/RegisteredNamedMethods.ts b/src/ui/Base/RegisteredNamedMethods.ts index 7b9071590b..4a5d34d829 100644 --- a/src/ui/Base/RegisteredNamedMethods.ts +++ b/src/ui/Base/RegisteredNamedMethods.ts @@ -205,18 +205,24 @@ export function get(element: HTMLElement, componentClass?, noThrow?: boolean): B return Component.get(element, componentClass, noThrow); } -Initialization.registerNamedMethod('get', (element: HTMLElement, componentClass?: any, noThrow?: boolean): BaseComponent => { - return get(element, componentClass, noThrow); -}); +Initialization.registerNamedMethod( + 'get', + (element: HTMLElement, componentClass?: any, noThrow?: boolean): BaseComponent => { + return get(element, componentClass, noThrow); + } +); export function result(element: HTMLElement, noThrow?: boolean): IQueryResult { Assert.exists(element); return Component.getResult(element, noThrow); } -Initialization.registerNamedMethod('result', (element: HTMLElement, noThrow?: boolean): IQueryResult => { - return result(element, noThrow); -}); +Initialization.registerNamedMethod( + 'result', + (element: HTMLElement, noThrow?: boolean): IQueryResult => { + return result(element, noThrow); + } +); function getCoveoAnalyticsClient(element: HTMLElement): IAnalyticsClient { var analytics = getCoveoAnalytics(element); diff --git a/src/ui/CardActionBar/CardActionBar.ts b/src/ui/CardActionBar/CardActionBar.ts index 4a38ab53c6..c8d541d19f 100644 --- a/src/ui/CardActionBar/CardActionBar.ts +++ b/src/ui/CardActionBar/CardActionBar.ts @@ -133,7 +133,11 @@ export class CardActionBar extends Component { private appendArrow() { this.arrowContainer = $$('div', { className: 'coveo-card-action-bar-arrow-container', tabindex: 0 }).el; - this.bind.on(this.arrowContainer, 'keyup', KeyboardUtils.keypressAction(KEYBOARD.ENTER, () => this.show())); + this.bind.on( + this.arrowContainer, + 'keyup', + KeyboardUtils.keypressAction(KEYBOARD.ENTER, () => this.show()) + ); const arrowUp = $$('span', { className: 'coveo-icon coveo-card-action-bar-arrow-icon' }, SVGIcons.icons.arrowUp); SVGDom.addClassToSVGInContainer(arrowUp.el, 'coveo-card-action-bar-arrow-svg'); this.arrowContainer.appendChild(arrowUp.el); diff --git a/src/ui/Quickview/Quickview.ts b/src/ui/Quickview/Quickview.ts index 87189c6d5b..40a01aea91 100644 --- a/src/ui/Quickview/Quickview.ts +++ b/src/ui/Quickview/Quickview.ts @@ -409,12 +409,7 @@ export class Quickview extends Component { if (typeof result.hasHtmlVersion == 'undefined' || result.hasHtmlVersion || this.options.alwaysShow) { const clickAction = () => this.open(); - new AccessibleButton() - .withElement(this.element) - .withSelectAction(clickAction) - .withLabel(l('Quickview')) - .withOwner(this.bind) - .build(); + new AccessibleButton().withElement(this.element).withSelectAction(clickAction).withLabel(l('Quickview')).withOwner(this.bind).build(); } else { this.element.style.display = 'none'; } diff --git a/unitTests/Fake.ts b/unitTests/Fake.ts index dc0c1f2e4d..173d5941ba 100644 --- a/unitTests/Fake.ts +++ b/unitTests/Fake.ts @@ -365,7 +365,8 @@ export class FakeResults { pageNumber: 0, advancedQuery: token + 'advancedQuery', didYouMean: false, - contextual: false + contextual: false, + clientId: token + 'clientId' }; } @@ -393,7 +394,8 @@ export class FakeResults { sourceName: token + 'sourceName', documentPosition: 0, viewMethod: token + 'viewMethod', - rankingModifier: token + 'rankingModifier' + rankingModifier: token + 'rankingModifier', + clientId: token + 'clientId' }; } diff --git a/unitTests/MockCookie.ts b/unitTests/MockCookie.ts new file mode 100644 index 0000000000..cdcbaf63f3 --- /dev/null +++ b/unitTests/MockCookie.ts @@ -0,0 +1,52 @@ +function mapToObject(list: string[], predicate: (value: string) => [string, string]): T { + const obj = {}; + list.forEach(listMember => { + const [key, value] = predicate(listMember); + obj[key] = value; + }); + return obj as T; +} + +const cookiesDescriptor = + Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie'); + +export class MockCookie { + private cookiesDict: { [cookieName: string]: string } = {}; + + static mockBrowserCookies() { + if (!cookiesDescriptor || !cookiesDescriptor.configurable) { + return false; + } + const mockCookie = new MockCookie(); + Object.defineProperty(document, 'cookie', { + get: () => mockCookie.cookie, + set: value => (mockCookie.cookie = value) + }); + return true; + } + + static unmockBrowserCookies() { + if (!cookiesDescriptor || !cookiesDescriptor.configurable) { + return false; + } + Object.defineProperty(document, 'cookie', cookiesDescriptor); + return true; + } + + public get cookie() { + return Object.keys(this.cookiesDict) + .map(key => `${key}=${this.cookiesDict[key]}`) + .join('; '); + } + + public set cookie(cookie: string) { + const assignments = cookie.split(';').map(cookie => cookie.trim()); + const [cookieName, cookieValue] = assignments[0].split('='); + const properties = mapToObject(assignments.slice(1), assignment => assignment.split('=') as [string, string]); + if ('expiration' in properties && parseInt(properties.expiration, 10) < Date.now()) { + delete properties.expiration; + } else { + this.cookiesDict[cookieName] = cookieValue; + } + } +} diff --git a/unitTests/Test.ts b/unitTests/Test.ts index cfba25e4e5..c35860a0af 100644 --- a/unitTests/Test.ts +++ b/unitTests/Test.ts @@ -6,6 +6,9 @@ if (Simulate.isChromeHeadless()) { Logger.disable(); } +import { MockCookie } from './MockCookie'; +MockCookie.mockBrowserCookies(); + import { defaultLanguage } from '../src/strings/DefaultLanguage'; defaultLanguage(); @@ -866,3 +869,6 @@ FacetSearchElementTest(); import { HistoryStoreTest } from './utils/HistoryStoreTest'; HistoryStoreTest(); + +import { AnalyticsInformationTest } from './ui/AnalyticsInformationTest'; +AnalyticsInformationTest(); diff --git a/unitTests/rest/EndpointCallerTest.ts b/unitTests/rest/EndpointCallerTest.ts index efc5d9622b..7a075ebafe 100644 --- a/unitTests/rest/EndpointCallerTest.ts +++ b/unitTests/rest/EndpointCallerTest.ts @@ -3,17 +3,17 @@ import { IQueryResults } from '../../src/rest/QueryResults'; import { FakeResults } from '../Fake'; export function EndpointCallerTest() { - describe('EndpointCaller', function() { - describe('using generic call', function() { - beforeEach(function() { + describe('EndpointCaller', function () { + describe('using generic call', function () { + beforeEach(function () { jasmine.Ajax.install(); }); - afterEach(function() { + afterEach(function () { jasmine.Ajax.uninstall(); }); - it('should use XMLHTTPRequest by default', function() { + it('should use XMLHTTPRequest by default', function () { var endpointCaller = new EndpointCaller(); endpointCaller.call({ method: 'POST', @@ -26,7 +26,7 @@ export function EndpointCallerTest() { expect(jasmine.Ajax.requests.mostRecent().url).toBe('this is an XMLHTTPRequest'); }); - it('should use the provided XMLHTTPRequest', function() { + it('should use the provided XMLHTTPRequest', function () { class CustomXMLHttpRequest extends XMLHttpRequest {} var endpointCaller = new EndpointCaller({ xmlHttpRequest: CustomXMLHttpRequest }); @@ -41,7 +41,7 @@ export function EndpointCallerTest() { expect(jasmine.Ajax.requests.mostRecent() instanceof CustomXMLHttpRequest).toBe(true); }); - it('should set the auth if provided', function() { + it('should set the auth if provided', function () { var endpointCaller = new EndpointCaller({ accessToken: 'myToken' }); @@ -71,16 +71,16 @@ export function EndpointCallerTest() { }); }); - describe('using XMLHTTPRequest', function() { - beforeEach(function() { + describe('using XMLHTTPRequest', function () { + beforeEach(function () { jasmine.Ajax.install(); }); - afterEach(function() { + afterEach(function () { jasmine.Ajax.uninstall(); }); - it('should set the correct requested params on the XMLHTTPRequest', function() { + it('should set the correct requested params on the XMLHTTPRequest', function () { var endpointCaller = new EndpointCaller(); endpointCaller.call({ method: 'POST', @@ -132,8 +132,8 @@ export function EndpointCallerTest() { expect(Object.keys(fakeRequest.requestHeaders).length).toBe(0); }); - describe('using response type text', function() { - beforeEach(function() { + describe('using response type text', function () { + beforeEach(function () { this.endpointCaller = new EndpointCaller(); this.promise = this.endpointCaller.call({ method: 'POST', @@ -149,12 +149,12 @@ export function EndpointCallerTest() { }); }); - afterEach(function() { + afterEach(function () { this.endpointCaller = undefined; this.promise = undefined; }); - it('should work if responseContentType is text', function(done) { + it('should work if responseContentType is text', function (done) { this.promise .then((response: ISuccessResponse) => { expect(response.data.results.length).toBe(10); @@ -171,7 +171,7 @@ export function EndpointCallerTest() { }); }); - it('should work if responseContentType is application/json', function(done) { + it('should work if responseContentType is application/json', function (done) { this.promise .then((response: ISuccessResponse) => { expect(response.data.results.length).toBe(10); @@ -188,7 +188,7 @@ export function EndpointCallerTest() { }); }); - it('should behave properly if there is an error', function(done) { + it('should behave properly if there is an error', function (done) { this.promise .then((response: ISuccessResponse) => { // This should never execute, and always go to the catch statement @@ -207,7 +207,7 @@ export function EndpointCallerTest() { }); }); - it('should behave properly if there is an error in the body', function(done) { + it('should behave properly if there is an error in the body', function (done) { this.promise .then((response: ISuccessResponse) => { // This should never execute, and always go to the catch statement @@ -231,8 +231,8 @@ export function EndpointCallerTest() { }); }); - describe('using response type json', function() { - beforeEach(function() { + describe('using response type json', function () { + beforeEach(function () { this.endpointCaller = new EndpointCaller(); this.promise = this.endpointCaller.call({ method: 'POST', @@ -247,12 +247,12 @@ export function EndpointCallerTest() { errorsAsSuccess: false }); }); - afterEach(function() { + afterEach(function () { this.endpointCaller = undefined; this.promise = undefined; }); - it('should work if responseContentType is text', function(done) { + it('should work if responseContentType is text', function (done) { this.promise .then((response: ISuccessResponse) => { expect(response.data.results.length).toBe(10); @@ -270,7 +270,7 @@ export function EndpointCallerTest() { }); }); - it('should work if responseContentType is application/json', function(done) { + it('should work if responseContentType is application/json', function (done) { this.promise .then((response: ISuccessResponse) => { expect(response.data.results.length).toBe(10); @@ -289,7 +289,7 @@ export function EndpointCallerTest() { }); }); - it('should allow to modify the request with an option', function() { + it('should allow to modify the request with an option', function () { let endpointCaller = new EndpointCaller({ accessToken: 'myToken', requestModifier: requestInfo => { diff --git a/unitTests/rest/SearchEndpointTest.ts b/unitTests/rest/SearchEndpointTest.ts index 8c32ae5466..07fd50ed60 100644 --- a/unitTests/rest/SearchEndpointTest.ts +++ b/unitTests/rest/SearchEndpointTest.ts @@ -15,11 +15,68 @@ import _ = require('underscore'); import { Utils } from '../../src/utils/Utils'; import { IFacetSearchResponse } from '../../src/rest/Facet/FacetSearchResponse'; import { ExecutionPlan } from '../../src/rest/Plan'; +import { buildHistoryStore } from '../../src/utils/HistoryStore'; +import { Cookie } from '../../src/Core'; + +interface ILastSentAnalytics { + visitorId: string; + clientId: string; + documentLocation: string; + documentReferrer: string; + pageId: string; +} export function SearchEndpointTest() { describe('SearchEndpoint', () => { + const visitorId = 'the imposter'; + const clientId = 'some random uuid'; + const pageId = 'some pretty home page'; + + function fakeHistoryStore() { + const store = buildHistoryStore(); + store.clear(); + store.addElement({ time: new Date().toTimeString(), internalTime: Date.now(), name: 'PageView', value: pageId }); + store.addElement({ time: new Date().toTimeString(), internalTime: Date.now(), name: 'PageVieww', value: 'abc' }); + } + + function fakeCookies() { + Cookie.set('visitorId', visitorId); + Cookie.set('clientId', clientId); + } + + function fakeClientInformation() { + fakeHistoryStore(); + fakeCookies(); + } + + const expectedAnalytics: ILastSentAnalytics = { + visitorId, + clientId, + documentReferrer: document.referrer, + documentLocation: document.location.href, + pageId + }; + function getLastSentAnalyticsFromURI(): ILastSentAnalytics { + const lastParams = jasmine.Ajax.requests.mostRecent().params; + const analyticsMatch = /analytics=([^&]*)&/.exec(lastParams); + const visitorIdMatch = /visitorId=([^&]*)&/.exec(lastParams); + return { + ...JSON.parse(decodeURIComponent(analyticsMatch[1])), + visitorId: decodeURIComponent(visitorIdMatch[1]) + }; + } + + function getLastSentAnalyticsFromJSON(): ILastSentAnalytics { + const lastParams = JSON.parse(jasmine.Ajax.requests.mostRecent().params); + return { + ...lastParams.analytics, + visitorId: lastParams.visitorId + }; + } + beforeEach(() => { SearchEndpoint.endpoints = {}; + fakeClientInformation(); }); afterEach(() => { @@ -323,6 +380,9 @@ export function SearchEndpointTest() { expect(jasmine.Ajax.requests.mostRecent().params).toContain('numberOfResults=153'); expect(jasmine.Ajax.requests.mostRecent().params).toContain('enableCollaborativeRating=true'); expect(jasmine.Ajax.requests.mostRecent().params).toContain('actionsHistory='); + const analytics = getLastSentAnalyticsFromURI(); + expect(analytics).toEqual(expectedAnalytics); + expect(jasmine.Ajax.requests.mostRecent().params); expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST'); promiseSuccess.then((data: IQueryResults) => { expect(data.results.length).toBe(10); @@ -404,10 +464,13 @@ export function SearchEndpointTest() { q: 'batman', numberOfResults: 153, enableCollaborativeRating: true, - actionsHistory: [] + actionsHistory: buildHistoryStore().getHistory() }) ); + const analytics = getLastSentAnalyticsFromJSON(); + expect(analytics).toEqual(expectedAnalytics); + mockResponse(done); }); }); @@ -697,6 +760,9 @@ export function SearchEndpointTest() { expect(jasmine.Ajax.requests.mostRecent().params).toContain('actionsHistory='); expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST'); + const analytics = getLastSentAnalyticsFromURI(); + expect(analytics).toEqual(expectedAnalytics); + // Not real extensions, but will suffice for test purpose promiseSuccess .then((response: IQuerySuggestResponse) => { @@ -754,6 +820,9 @@ export function SearchEndpointTest() { expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST'); expect(JSON.parse(jasmine.Ajax.requests.mostRecent().params)).toEqual(jasmine.objectContaining({ field: 'test' })); + const analytics = getLastSentAnalyticsFromJSON(); + expect(analytics).toEqual(expectedAnalytics); + // Not real extensions, but will suffice for test purpose promiseSuccess .then((response: IFacetSearchResponse) => { diff --git a/unitTests/ui/AnalyticsInformationTest.ts b/unitTests/ui/AnalyticsInformationTest.ts new file mode 100644 index 0000000000..37499a9537 --- /dev/null +++ b/unitTests/ui/AnalyticsInformationTest.ts @@ -0,0 +1,93 @@ +import { Cookie } from '../../src/Core'; +import { AnalyticsInformation } from '../../src/ui/Analytics/AnalyticsInformation'; +import { buildHistoryStore } from '../../src/utils/HistoryStore'; + +type HistoryStore = ReturnType; + +export function AnalyticsInformationTest() { + describe('AnalyticsInformation', () => { + let historyStore: HistoryStore; + function addHistoryElement(name: string, value: string) { + historyStore.addElement({ + time: new Date().toTimeString(), + internalTime: Date.now(), + name, + value + }); + } + + let analyticsInformation: AnalyticsInformation; + beforeEach(() => { + analyticsInformation = new AnalyticsInformation(); + historyStore = buildHistoryStore(); + historyStore.clear(); + analyticsInformation.clearCookies(); + }); + + describe('without cookies', () => { + it("doesn't have a visitorId", () => { + expect(analyticsInformation.visitorId).toBeNull(); + }); + + it("doesn't have a clientId", () => { + expect(analyticsInformation.clientId).toBeNull(); + }); + + it('has a location', () => { + expect(analyticsInformation.location).toEqual(document.location.href); + }); + + it('has a referrer', () => { + expect(analyticsInformation.referrer).toEqual(document.referrer); + }); + }); + + describe('with cookies', () => { + const visitorId = 'def'; + const clientId = 'abc'; + beforeEach(() => { + Cookie.set('visitorId', visitorId); + Cookie.set('clientId', clientId); + }); + + it('has a visitorId', () => { + console.log(`Current visitorId D: "${Cookie.get('visitorId')}"`); + expect(analyticsInformation.visitorId).toEqual(visitorId); + }); + + it('has a clientId', () => { + expect(analyticsInformation.clientId).toEqual(clientId); + 4; + }); + + it('has a location', () => { + expect(analyticsInformation.location).toEqual(document.location.href); + }); + + it('has a referrer', () => { + expect(analyticsInformation.referrer).toEqual(document.referrer); + }); + }); + + describe('without a PageView event', () => { + beforeEach(() => { + addHistoryElement('PageVieww', 'abc'); + }); + + it("doesn't have a pageId", () => { + expect(analyticsInformation.lastPageId).toBeNull(); + }); + }); + + describe('with a PageView event', () => { + const pageId = 'ghi'; + beforeEach(() => { + addHistoryElement('PageView', pageId); + }); + + it('has a pageId', () => { + expect(analyticsInformation.lastPageId).toEqual(pageId); + }); + }); + }); +} diff --git a/unitTests/ui/CardActionBarTest.ts b/unitTests/ui/CardActionBarTest.ts index 639c0b65ef..68abf33479 100644 --- a/unitTests/ui/CardActionBarTest.ts +++ b/unitTests/ui/CardActionBarTest.ts @@ -7,7 +7,7 @@ export function CardActionBarTest() { let test: Mock.IBasicComponentSetup; let parentResult: HTMLElement; - beforeEach(function() { + beforeEach(function () { test = Mock.advancedComponentSetup(CardActionBar, { modifyBuilder: b => { parentResult = $$('div', { className: 'CoveoResult' }, $$('div')).el; @@ -17,9 +17,9 @@ export function CardActionBarTest() { }); afterEach(() => (parentResult = null)); - describe('exposes option', function() { - describe('hidden set to true', function() { - beforeEach(function() { + describe('exposes option', function () { + describe('hidden set to true', function () { + beforeEach(function () { test = Mock.advancedComponentSetup(CardActionBar, { modifyBuilder: b => { parentResult = $$('div', { className: 'CoveoResult' }, $$('div')).el; @@ -31,37 +31,37 @@ export function CardActionBarTest() { }); }); - it('should show when parent result is clicked', function() { + it('should show when parent result is clicked', function () { spyOn(test.cmp, 'show'); $$(parentResult).trigger('click'); expect(test.cmp.show).toHaveBeenCalledTimes(1); }); - it('should hide when mouse leaves parent result', function() { + it('should hide when mouse leaves parent result', function () { spyOn(test.cmp, 'hide'); $$(parentResult).trigger('mouseleave'); expect(test.cmp.hide).toHaveBeenCalledTimes(1); }); - it('should display an arrow indicator in its parent CoveoResult', function() { + it('should display an arrow indicator in its parent CoveoResult', function () { expect($$(parentResult).find('.coveo-card-action-bar-arrow-container')).not.toBeNull(); }); - it('should show when action bar is focused in', function() { + it('should show when action bar is focused in', function () { spyOn(test.cmp, 'show'); $$(test.cmp.element).trigger('focusin'); expect(test.cmp.show).toHaveBeenCalledTimes(1); }); - it('should hide when action bar is focused out', function() { + it('should hide when action bar is focused out', function () { spyOn(test.cmp, 'hide'); $$(test.cmp.element).trigger('focusout'); expect(test.cmp.hide).toHaveBeenCalledTimes(1); }); }); - describe('hidden set to false', function() { - beforeEach(function() { + describe('hidden set to false', function () { + beforeEach(function () { test = Mock.advancedComponentSetup(CardActionBar, { modifyBuilder: b => { parentResult = $$('div', { className: 'CoveoResult' }, $$('div')).el; @@ -73,18 +73,18 @@ export function CardActionBarTest() { }); }); - it('should always be shown', function() { + it('should always be shown', function () { spyOn(test.cmp, 'show'); $$(parentResult).trigger('click'); expect(test.cmp.show).not.toHaveBeenCalled(); }); - it('should not display an arrow indicator in its parent CoveoResult', function() { + it('should not display an arrow indicator in its parent CoveoResult', function () { expect($$(parentResult).find('.coveo-card-action-bar-arrow-container')).toBeNull(); }); }); - it('openOnMouseOver set to true should call open when mouseentering the arrow container', function() { + it('openOnMouseOver set to true should call open when mouseentering the arrow container', function () { test = Mock.advancedComponentSetup(CardActionBar, { modifyBuilder: b => { parentResult = $$('div', { className: 'CoveoResult' }, $$('div')).el; @@ -100,7 +100,7 @@ export function CardActionBarTest() { expect(test.cmp.show).toHaveBeenCalledTimes(1); }); - it('openOnMouseOver set to false should not call open when mouseentering the arrow container', function() { + it('openOnMouseOver set to false should not call open when mouseentering the arrow container', function () { test = Mock.advancedComponentSetup(CardActionBar, { modifyBuilder: b => { parentResult = $$('div', { className: 'CoveoResult' }, $$('div')).el; @@ -117,12 +117,12 @@ export function CardActionBarTest() { }); }); - it('show should add coveo-opened class', function() { + it('show should add coveo-opened class', function () { test.cmp.show(); expect($$(test.cmp.element).hasClass('coveo-opened')).toBe(true); }); - it('hide should remove coveo-opened class', function() { + it('hide should remove coveo-opened class', function () { test.cmp.show(); test.cmp.hide(); expect($$(test.cmp.element).hasClass('coveo-opened')).toBe(false); diff --git a/unitTests/ui/CheckboxTest.ts b/unitTests/ui/CheckboxTest.ts index 81e8517c0a..09dc4af92f 100644 --- a/unitTests/ui/CheckboxTest.ts +++ b/unitTests/ui/CheckboxTest.ts @@ -38,19 +38,11 @@ export function CheckboxTest() { }); it('the checkbox input has an aria-hidden equal to true', () => { - expect( - $$(checkbox.getElement()) - .find('input') - .getAttribute('aria-hidden') - ).toBe('true'); + expect($$(checkbox.getElement()).find('input').getAttribute('aria-hidden')).toBe('true'); }); it('the checkbox input has an aria-label', () => { - expect( - $$(checkbox.getElement()) - .find('input') - .getAttribute('aria-label') - ).toEqual(ariaLabel); + expect($$(checkbox.getElement()).find('input').getAttribute('aria-label')).toEqual(ariaLabel); }); it("the button's aria-label is its ariaLabel parameter", () => { diff --git a/unitTests/ui/ExplanationModalTest.ts b/unitTests/ui/ExplanationModalTest.ts index 73b9bb9428..7f9ee433ba 100644 --- a/unitTests/ui/ExplanationModalTest.ts +++ b/unitTests/ui/ExplanationModalTest.ts @@ -40,9 +40,9 @@ export function ExplanationModalTest() { function initializeModal(firstReasonHasDetails = true) { explanationModal = new ExplanationModal({ - reasons: (reasons = createReason(firstReasonHasDetails)), - onClosed: (onClosed = jasmine.createSpy('onClosed')), - ownerElement: (ownerElement = $$('div').el), + reasons: reasons = createReason(firstReasonHasDetails), + onClosed: onClosed = jasmine.createSpy('onClosed'), + ownerElement: ownerElement = $$('div').el, modalBoxModule: Simulate.modalBoxModule() }); explanationModal['modal'] = accessibleModal = mock(AccessibleModal); diff --git a/unitTests/ui/ExportToExcelTest.ts b/unitTests/ui/ExportToExcelTest.ts index 9e643b8ded..7ad88627b5 100644 --- a/unitTests/ui/ExportToExcelTest.ts +++ b/unitTests/ui/ExportToExcelTest.ts @@ -4,7 +4,7 @@ import { IExportToExcelOptions } from '../../src/ui/ExportToExcel/ExportToExcel' import { analyticsActionCauseList } from '../../src/ui/Analytics/AnalyticsActionListMeta'; export function ExportToExcelTest() { - describe('ExportToExcel', function() { + describe('ExportToExcel', function () { let options: IExportToExcelOptions = {}; let test: Mock.IBasicComponentSetup; @@ -19,7 +19,7 @@ export function ExportToExcelTest() { return a; } - beforeEach(function() { + beforeEach(function () { options = {}; setCreateAnchor(() => buildAnchorWithClickSpy()); @@ -92,8 +92,8 @@ export function ExportToExcelTest() { }); }); - describe('exposes options', function() { - it('numberOfResults calls search endpoint with appropriate number of results', function() { + describe('exposes options', function () { + it('numberOfResults calls search endpoint with appropriate number of results', function () { options.numberOfResults = 200; initExportToExcel(); diff --git a/unitTests/ui/FacetTest.ts b/unitTests/ui/FacetTest.ts index 04d61e8bf4..f5c741b925 100644 --- a/unitTests/ui/FacetTest.ts +++ b/unitTests/ui/FacetTest.ts @@ -843,7 +843,7 @@ export function FacetTest() { }); }); - it('facetSearchDelay should be passed to the facet search component', function(done) { + it('facetSearchDelay should be passed to the facet search component', function (done) { test = Mock.optionsComponentSetup(Facet, { field: '@field', facetSearchDelay: 5 @@ -856,7 +856,7 @@ export function FacetTest() { }, 6); // one more ms then facetSearchDelay }); - it('numberOfValuesInFacetSearch should be passed to the facet search component', function(done) { + it('numberOfValuesInFacetSearch should be passed to the facet search component', function (done) { test = Mock.optionsComponentSetup(Facet, { field: '@field', numberOfValuesInFacetSearch: 13 diff --git a/unitTests/ui/RegisteredNamedMethodsTest.ts b/unitTests/ui/RegisteredNamedMethodsTest.ts index 49f5cf7700..afd7eb5315 100644 --- a/unitTests/ui/RegisteredNamedMethodsTest.ts +++ b/unitTests/ui/RegisteredNamedMethodsTest.ts @@ -313,10 +313,10 @@ export function RegisteredNamedMethodsTest() { expect(setup.env.queryController.resetHistory).toHaveBeenCalled(); }); - it('should clear visitor cookie when disabling analytics', () => { - const clearCookiesFunction = spyOn(setup.cmp.client.endpoint, 'clearCookies'); + it('should call clear local data on analytics when disabling analytics', () => { + const clearLocalDataSpy = (setup.cmp['clearLocalData'] = jasmine.createSpy('clearLocalData')); RegisteredNamedMethod.disableAnalytics(setup.env.root); - expect(clearCookiesFunction).toHaveBeenCalled(); + expect(clearLocalDataSpy).toHaveBeenCalled(); }); it('should clear actions history when clearing local data', () => { @@ -324,10 +324,10 @@ export function RegisteredNamedMethodsTest() { expect(setup.env.queryController.resetHistory).toHaveBeenCalled(); }); - it('should clear visitor cookie when clearing local data', () => { - const clearCookiesFunction = spyOn(setup.cmp.client.endpoint, 'clearCookies'); + it('should call clear local data on analytics when clearing local data', () => { + const clearLocalDataSpy = (setup.cmp['clearLocalData'] = jasmine.createSpy('clearLocalData')); RegisteredNamedMethod.clearLocalData(setup.env.root); - expect(clearCookiesFunction).toHaveBeenCalled(); + expect(clearLocalDataSpy).toHaveBeenCalled(); }); describe('with an Analytics component and a Searchbox component', () => { diff --git a/unitTests/ui/SearchInterfaceTest.ts b/unitTests/ui/SearchInterfaceTest.ts index ddcc85c457..6c3b0c7541 100644 --- a/unitTests/ui/SearchInterfaceTest.ts +++ b/unitTests/ui/SearchInterfaceTest.ts @@ -463,7 +463,7 @@ export function SearchInterfaceTest() { expect(simulation.queryBuilder.timezone).toBe('aa-bb'); }); - it('enableDebugInfo should create a debug component', function(done) { + it('enableDebugInfo should create a debug component', function (done) { const cmp = new SearchInterface( div, { @@ -478,7 +478,7 @@ export function SearchInterfaceTest() { }); }); - it('enableDebugInfo disabled should not create a debug component', function(done) { + it('enableDebugInfo disabled should not create a debug component', function (done) { const cmp = new SearchInterface( div, { diff --git a/unitTests/utils/CookieUtilsTest.ts b/unitTests/utils/CookieUtilsTest.ts index c4affddf1a..6a2e15b293 100644 --- a/unitTests/utils/CookieUtilsTest.ts +++ b/unitTests/utils/CookieUtilsTest.ts @@ -2,27 +2,7 @@ import { Cookie } from '../../src/utils/CookieUtils'; import { Simulate } from '../Simulate'; export function CookieUtilsTest() { - describe('CookieUtils', function() { - var mockDocument = { - cookie: '' - }; - var cookieDesc = - Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie'); - if (cookieDesc && cookieDesc.configurable) { - Object.defineProperty(document, 'cookie', { - get: function() { - return mockDocument.cookie; - }, - set: function(val) { - mockDocument.cookie = val; - } - }); - } - - afterEach(function() { - mockDocument.cookie = ''; - }); - + describe('CookieUtils', function () { it('sets a cookie accordingly', () => { Cookie.set('foo', 'bar'); expect(document.cookie.indexOf('foo=bar')).not.toBe(-1); diff --git a/unitTests/utils/DependsOnManagerTest.ts b/unitTests/utils/DependsOnManagerTest.ts index bd9c597d54..923adb5df8 100644 --- a/unitTests/utils/DependsOnManagerTest.ts +++ b/unitTests/utils/DependsOnManagerTest.ts @@ -50,7 +50,7 @@ export function DependsOnManagerTest() { } function assignCustomCondition() { - dependentMock.facet.ref.options.dependsOnCondition = function(facet) { + dependentMock.facet.ref.options.dependsOnCondition = function (facet) { const currentValues = facet.queryStateModel.get(QueryStateModel.getFacetId(facet.options.id)); return currentValues && currentValues.indexOf('the right value') !== -1; }; From 1455533d8bc3e4f7209050a266818b1865730c8b Mon Sep 17 00:00:00 2001 From: Travis CI Date: Thu, 5 Nov 2020 22:55:33 +0000 Subject: [PATCH 12/25] Automatically created by Travis CI --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a71948ae33..eae7030f47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.752", + "version": "2.0.753", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", @@ -181,4 +181,4 @@ "@types/node": "~10.5.4" }, "snyk": true -} +} \ No newline at end of file From cd7f972e49e5ff8bc411eda9473d53cb1c878b62 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Fri, 6 Nov 2020 22:41:08 +0000 Subject: [PATCH 13/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index eae7030f47..55535998bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.753", + "version": "2.0.754", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From e4fbd2fcd662b4e2101ccb44f09ec04c228bd25c Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sat, 7 Nov 2020 22:16:44 +0000 Subject: [PATCH 14/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55535998bb..af3147dc4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.754", + "version": "2.0.755", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From e9a981ee959312660e69bd987c11d8b0b86f2ce4 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sun, 8 Nov 2020 22:23:34 +0000 Subject: [PATCH 15/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index af3147dc4a..406d5963e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.755", + "version": "2.0.756", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From f5bae139d1fef5c0d32263b900ffc5d4604a9b58 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Mon, 9 Nov 2020 23:18:37 +0000 Subject: [PATCH 16/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 406d5963e6..d2c12a4e46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.756", + "version": "2.0.757", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From f24f95ae093844c3776b48735ffe4e38675ddda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-F=C3=A9lix=20Thibodeau?= Date: Tue, 10 Nov 2020 09:19:21 -0500 Subject: [PATCH 17/25] Only replace noResultsFoundMessage with "No result" when the query is empty (#1650) https://coveord.atlassian.net/browse/JSUI-3148 --- src/ui/QuerySummary/QuerySummary.ts | 16 +++++++++------- unitTests/ui/QuerySummaryTest.ts | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ui/QuerySummary/QuerySummary.ts b/src/ui/QuerySummary/QuerySummary.ts index 6729a18b93..d1ac204c2b 100644 --- a/src/ui/QuerySummary/QuerySummary.ts +++ b/src/ui/QuerySummary/QuerySummary.ts @@ -190,10 +190,6 @@ export class QuerySummary extends Component { } private replaceQueryTagsWithHighlightedQuery(template: string) { - if (this.sanitizedQuery.trim() === '') { - return l('noResult'); - } - const highlightedQuery = `${this.sanitizedQuery}`; return QuerySummaryUtils.replaceQueryTags(template, highlightedQuery); } @@ -245,15 +241,21 @@ export class QuerySummary extends Component { return $$(this.element).findAll(`.${noResultsCssClass}`); } - private getNoResultsFoundMessageElement() { - const parsedNoResultsFoundMessage = this.replaceQueryTagsWithHighlightedQuery(this.options.noResultsFoundMessage); + private get parsedNoResultsFoundMessage() { + if (this.sanitizedQuery.trim() === '') { + return l('noResult'); + } + return this.replaceQueryTagsWithHighlightedQuery(this.options.noResultsFoundMessage); + } + + private getNoResultsFoundMessageElement() { const noResultsFoundMessage = $$( 'div', { className: 'coveo-query-summary-no-results-string' }, - parsedNoResultsFoundMessage + this.parsedNoResultsFoundMessage ); return noResultsFoundMessage; diff --git a/unitTests/ui/QuerySummaryTest.ts b/unitTests/ui/QuerySummaryTest.ts index d0927dd9f5..7881ffceda 100644 --- a/unitTests/ui/QuerySummaryTest.ts +++ b/unitTests/ui/QuerySummaryTest.ts @@ -274,7 +274,7 @@ export function QuerySummaryTest() { expect(getCustomMessageElement().textContent).toBe('custom querySearched Message querySearched'); }); - it(`when mutiple no results page are triggered consicutively with different querySearched, + it(`when mutiple no results page are triggered consecutively with different querySearched, it should update the query tags with the new query searched`, () => { test = Mock.optionsComponentSetup(QuerySummary, { enableNoResultsFoundMessage: true, @@ -384,7 +384,7 @@ export function QuerySummaryTest() { expect(getcustomNoResultsPageElement().textContent).toBe('querySearched querySearched'); }); - it(`when mutiple no results page are triggered consicutively with different querySearched, + it(`when mutiple no results page are triggered consecutively with different querySearched, it should update the query tags with the new query searched`, () => { test.cmp.element.innerHTML = `
${queryTag}
`; @@ -410,12 +410,12 @@ export function QuerySummaryTest() { }); it(`when the query searched is an empty string, - it should put the string NoResult instead`, () => { + it should replace the query tag with the query searched`, () => { test.env.queryStateModel.get = () => ''; test.cmp.element.innerHTML = `
${queryTag}
`; Simulate.query(test.env, { results: FakeResults.createFakeResults(0) }); - expect(getcustomNoResultsPageElement().textContent).toBe('No results'); + expect(getcustomNoResultsPageElement().textContent).toBe(''); }); it(`when the query searched is the same as the queryTag, From 87c32a42588cdc7e669b465e5b394f9952d2e497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-F=C3=A9lix=20Thibodeau?= Date: Tue, 10 Nov 2020 15:12:00 -0500 Subject: [PATCH 18/25] Remove facet title vertical padding inside responsive move (#1651) https://coveord.atlassian.net/browse/JSUI-3149 --- sass/_ResponsiveFacets.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sass/_ResponsiveFacets.scss b/sass/_ResponsiveFacets.scss index 09f484e856..54390828ab 100644 --- a/sass/_ResponsiveFacets.scss +++ b/sass/_ResponsiveFacets.scss @@ -97,12 +97,15 @@ #{$allDynamicFacetsType} { margin: 0; - .coveo-dynamic-facet-header, .coveo-dynamic-facet-values, .coveo-dynamic-hierarchical-facet-values { padding: 5px; } + .coveo-dynamic-facet-header { + padding: 0 5px; + } + .coveo-dynamic-facet-search { margin: 10px 5px 0; } From 27f151dfac767286e726b279b1e48a1faa3d90f9 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Tue, 10 Nov 2020 23:27:19 +0000 Subject: [PATCH 19/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d2c12a4e46..6700363ecf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.757", + "version": "2.0.758", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From d9519a94281f2ecd5b3767effaa77918682bd3af Mon Sep 17 00:00:00 2001 From: Travis CI Date: Wed, 11 Nov 2020 23:07:23 +0000 Subject: [PATCH 20/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6700363ecf..ad962f7d19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.758", + "version": "2.0.759", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 38d8e424df2cf7001a4f88e56959c514c02c03f6 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Thu, 12 Nov 2020 23:21:38 +0000 Subject: [PATCH 21/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad962f7d19..55b2913160 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.759", + "version": "2.0.760", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 851f55580520720f25d73bf2368d735f58ae2adb Mon Sep 17 00:00:00 2001 From: Travis CI Date: Fri, 13 Nov 2020 23:02:48 +0000 Subject: [PATCH 22/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55b2913160..c7ed5874e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.760", + "version": "2.0.761", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 736d4c53cabd403dbd22675fc3dc199ec8f5e1ec Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sat, 14 Nov 2020 22:19:36 +0000 Subject: [PATCH 23/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c7ed5874e8..5073869904 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.761", + "version": "2.0.762", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 3b8eec100f13a4f013458e1b35272ac3e37571f7 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Sun, 15 Nov 2020 22:21:01 +0000 Subject: [PATCH 24/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5073869904..75ffda8603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.762", + "version": "2.0.763", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts", From 3afa5f0eab9d94a9f304b56ed187200f317bce57 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Mon, 16 Nov 2020 23:16:31 +0000 Subject: [PATCH 25/25] Automatically created by Travis CI --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75ffda8603..2560e63801 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coveo-search-ui", - "version": "2.0.763", + "version": "2.0.764", "description": "Coveo JavaScript Search Framework", "main": "./bin/js/CoveoJsSearch.js", "types": "./bin/ts/CoveoJsSearch.d.ts",