diff --git a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/__tests__/quanticBreadcrumbManager.test.js b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/__tests__/quanticBreadcrumbManager.test.js index 0a17eeee327..f5530407bb4 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/__tests__/quanticBreadcrumbManager.test.js +++ b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/__tests__/quanticBreadcrumbManager.test.js @@ -30,21 +30,25 @@ const functionsMocks = { }; const selectors = { - facetBreadcrumb: '[data-test="facet-breadcrumb"]', - facetBreadcrumbValue: '[data-test="facet-breadcrumb-value"]', - categoryFacetBreadcrumb: '[data-test="category-facet-breadcrumb"]', - categoryFacetBreadcrumbValue: '[data-test="category-facet-breadcrumb-value"]', - numericFacetBreadcrumb: '[data-test="numeric-facet-breadcrumb"]', - numericFacetBreadcrumbValue: '[data-test="numeric-facet-breadcrumb-value"]', - dateFacetBreadcrumb: '[data-test="date-facet-breadcrumb"]', - dateFacetBreadcrumbValue: '[data-test="date-facet-breadcrumb-value"]', + initializationError: 'c-quantic-component-error', + breadcrumbShowMoreButton: '[data-testid="breadcrumb-manager__more-button"]', + breadcrumbClearAllFiltersButton: + '[data-testid="breadcrumb-manager__clear-button"]', + facetBreadcrumb: '[data-testid="facet-breadcrumb"]', + facetBreadcrumbValue: '[data-testid="facet-breadcrumb-value"]', + categoryFacetBreadcrumb: '[data-testid="category-facet-breadcrumb"]', + categoryFacetBreadcrumbValue: + '[data-testid="category-facet-breadcrumb-value"]', + numericFacetBreadcrumb: '[data-testid="numeric-facet-breadcrumb"]', + numericFacetBreadcrumbValue: '[data-testid="numeric-facet-breadcrumb-value"]', + dateFacetBreadcrumb: '[data-testid="date-facet-breadcrumb"]', + dateFacetBreadcrumbValue: '[data-testid="date-facet-breadcrumb-value"]', }; const exampleEngine = { id: 'dummy engine', }; -const exampleFacetId = 'idOne'; - +const defaultCollapseThreshold = 5; let isInitialized = false; function createTestComponent(options = {}) { @@ -91,6 +95,15 @@ function mockSuccessfulHeadlessInitialization() { }; } +function mockErroneousHeadlessInitialization() { + // @ts-ignore + mockHeadlessLoader.initializeWithHeadless = (element) => { + if (element instanceof QuanticBreadcrumbManager) { + element.setInitializationError(); + } + }; +} + function cleanup() { // The jsdom instance is shared across test cases in a single file so reset the DOM while (document.body.firstChild) { @@ -101,6 +114,47 @@ function cleanup() { } describe('c-quantic-breadcrumb-manager', () => { + afterEach(() => { + breadcrumbManagerState = initialBreadcrumbManagerState; + storeState = initialStoreState; + cleanup(); + }); + + describe('when an initialization error occurs', () => { + beforeEach(() => { + mockErroneousHeadlessInitialization(); + }); + + it('should display the initialization error component', async () => { + const element = createTestComponent(); + await flushPromises(); + + const errorComponent = element.shadowRoot.querySelector( + selectors.initializationError + ); + + expect(errorComponent).not.toBeNull(); + }); + }); + + describe('controller initialization', () => { + beforeEach(() => { + mockSuccessfulHeadlessInitialization(); + prepareHeadlessState(); + }); + + it('should build the breadcrumb manager and subscribe to state', async () => { + createTestComponent(); + await flushPromises(); + + expect(functionsMocks.buildBreadcrumbManager).toHaveBeenCalledTimes(1); + expect(functionsMocks.buildBreadcrumbManager).toHaveBeenCalledWith( + exampleEngine + ); + expect(functionsMocks.subscribe).toHaveBeenCalledTimes(1); + }); + }); + beforeEach(() => { mockSuccessfulHeadlessInitialization(); prepareHeadlessState(); @@ -113,28 +167,33 @@ describe('c-quantic-breadcrumb-manager', () => { cleanup(); }); - describe('facet breadcrumbs', () => { + describe('facet breadcrumbs rendering', () => { const exampleFacetBreadcrumbs = [ { field: 'fieldOne', - facetId: exampleFacetId, - values: [{value: 'one'}, {value: 'two'}], + facetId: 'facetIdOne', + values: [{value: {value: 'one'}}, {value: {value: 'two'}}], + }, + { + field: 'fieldTwo', + facetId: 'facetIdTwo', + values: [{value: {value: 'three'}}, {value: {value: 'four'}}], }, ]; - beforeAll(() => { + beforeEach(() => { breadcrumbManagerState = { ...breadcrumbManagerState, facetBreadcrumbs: exampleFacetBreadcrumbs, hasBreadcrumbs: true, }; - - storeState = { - [exampleFacetId]: { - facetId: exampleFacetId, + storeState = exampleFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, format: (value) => value, - }, - }; + }; + return acc; + }, {}); }); it('should properly display the breadcrumb values', async () => { @@ -161,32 +220,196 @@ describe('c-quantic-breadcrumb-manager', () => { ); const values = exampleFacetBreadcrumb.values.map((item) => item.value); expect(labels).toEqual(values); + + const breadcrumbClearAllFiltersButton = + element.shadowRoot.querySelector( + selectors.breadcrumbClearAllFiltersButton + ); + expect(breadcrumbClearAllFiltersButton).not.toBeNull(); + }); + }); + + describe('when the number of selected values for one facet exceeds the collapse threshold', () => { + const testManyFacetBreadcrumbs = [ + { + field: 'fieldOne', + facetId: 'facetIdOne', + values: [ + {value: {value: 'one'}}, + {value: {value: 'two'}}, + {value: {value: 'three'}}, + {value: {value: 'four'}}, + {value: {value: 'five'}}, + {value: {value: 'six'}}, + ], + }, + ]; + + beforeEach(() => { + breadcrumbManagerState = { + ...breadcrumbManagerState, + facetBreadcrumbs: testManyFacetBreadcrumbs, + hasBreadcrumbs: true, + }; + storeState = testManyFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, + format: (value) => value, + }; + return acc; + }, {}); + }); + + it('should collapse the values and display a show more button', async () => { + const element = createTestComponent(); + await flushPromises(); + + const facetBreadcrumbs = element.shadowRoot.querySelectorAll( + selectors.facetBreadcrumb + ); + expect(facetBreadcrumbs.length).toBe(testManyFacetBreadcrumbs.length); + + const firstFacetBreadcrumb = facetBreadcrumbs[0]; + const facetBreadcrumbValues = Array.from( + firstFacetBreadcrumb.querySelectorAll(selectors.facetBreadcrumbValue) + ); + expect(facetBreadcrumbValues.length).toBe(defaultCollapseThreshold); + facetBreadcrumbValues.forEach((facetBreadcrumbValue, index) => { + const facetValueLabel = facetBreadcrumbValue.label.value; + expect(facetValueLabel).toBe( + testManyFacetBreadcrumbs[0].values[index].value.value + ); + }); + + const breadcrumbShowMoreButton = element.shadowRoot.querySelector( + selectors.breadcrumbShowMoreButton + ); + expect(breadcrumbShowMoreButton).not.toBeNull(); + }); + + it('should expand the values when the show more button is clicked', async () => { + const element = createTestComponent(); + await flushPromises(); + + const facetBreadcrumbs = element.shadowRoot.querySelectorAll( + selectors.facetBreadcrumb + ); + const firstFacetBreadcrumb = facetBreadcrumbs[0]; + + const facetBreadcrumbValues = Array.from( + firstFacetBreadcrumb.querySelectorAll(selectors.facetBreadcrumbValue) + ); + expect(facetBreadcrumbValues.length).toBe(defaultCollapseThreshold); + + const breadcrumbShowMoreButton = element.shadowRoot.querySelector( + selectors.breadcrumbShowMoreButton + ); + expect(breadcrumbShowMoreButton).not.toBeNull(); + + breadcrumbShowMoreButton.click(); + await flushPromises(); + + const facetBreadcrumbValuesAfterClick = Array.from( + firstFacetBreadcrumb.querySelectorAll(selectors.facetBreadcrumbValue) + ); + expect(facetBreadcrumbValuesAfterClick.length).toBe( + testManyFacetBreadcrumbs[0].values.length + ); + const breadcrumbShowMoreButtonAfterClick = + element.shadowRoot.querySelector(selectors.breadcrumbShowMoreButton); + expect(breadcrumbShowMoreButtonAfterClick).toBeNull(); + }); + }); + + describe('with a custom collapse threshold', () => { + const customCollapseThreshold = 3; + const testManyFacetBreadcrumbs = [ + { + field: 'fieldOne', + facetId: 'facetIdOne', + values: [ + {value: {value: 'one'}}, + {value: {value: 'two'}}, + {value: {value: 'three'}}, + {value: {value: 'four'}}, + {value: {value: 'five'}}, + {value: {value: 'six'}}, + ], + }, + ]; + + beforeEach(() => { + breadcrumbManagerState = { + ...breadcrumbManagerState, + facetBreadcrumbs: testManyFacetBreadcrumbs, + hasBreadcrumbs: true, + }; + storeState = testManyFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, + format: (value) => value, + }; + return acc; + }, {}); + }); + it('should collapse the values over the custom collapse threshold and display a show more button', async () => { + const element = createTestComponent({ + collapseThreshold: customCollapseThreshold, + }); + await flushPromises(); + + const facetBreadcrumbs = element.shadowRoot.querySelectorAll( + selectors.facetBreadcrumb + ); + expect(facetBreadcrumbs.length).toBe(testManyFacetBreadcrumbs.length); + + const firstFacetBreadcrumb = facetBreadcrumbs[0]; + const facetBreadcrumbValues = Array.from( + firstFacetBreadcrumb.querySelectorAll(selectors.facetBreadcrumbValue) + ); + expect(facetBreadcrumbValues.length).toBe(customCollapseThreshold); + facetBreadcrumbValues.forEach((facetBreadcrumbValue, index) => { + const facetValueLabel = facetBreadcrumbValue.label.value; + expect(facetValueLabel).toBe( + testManyFacetBreadcrumbs[0].values[index].value.value + ); + }); + + const breadcrumbShowMoreButton = element.shadowRoot.querySelector( + selectors.breadcrumbShowMoreButton + ); + expect(breadcrumbShowMoreButton).not.toBeNull(); }); }); }); describe('category facet breadcrumbs', () => { - const exampleCategoryFacetBreadcrumbs = [ + const testCategoryFacetBreadcrumbs = [ { - field: exampleFacetId, - facetId: exampleFacetId, + field: 'fieldOne', + facetId: 'facetIdOne', path: [{value: 'one'}, {value: 'two'}], }, + { + field: 'fieldTwo', + facetId: 'facetIdTwo', + path: [{value: 'three'}, {value: 'four'}], + }, ]; - beforeAll(() => { + beforeEach(() => { breadcrumbManagerState = { ...breadcrumbManagerState, - categoryFacetBreadcrumbs: exampleCategoryFacetBreadcrumbs, + categoryFacetBreadcrumbs: testCategoryFacetBreadcrumbs, hasBreadcrumbs: true, }; - - storeState = { - [exampleFacetId]: { - facetId: exampleFacetId, + storeState = testCategoryFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, format: (item) => item.value, - }, - }; + }; + return acc; + }, {}); }); it('should properly display the breadcrumb values', async () => { @@ -198,10 +421,10 @@ describe('c-quantic-breadcrumb-manager', () => { ); expect(categoryFacetBreadcrumbs.length).toBe( - exampleCategoryFacetBreadcrumbs.length + testCategoryFacetBreadcrumbs.length ); categoryFacetBreadcrumbs.forEach((categoryFacetBreadcrumb, index) => { - const exampleFacetBreadcrumb = exampleCategoryFacetBreadcrumbs[index]; + const exampleFacetBreadcrumb = testCategoryFacetBreadcrumbs[index]; const categoryFacetBreadcrumbValues = Array.from( categoryFacetBreadcrumb.querySelectorAll( selectors.categoryFacetBreadcrumbValue @@ -217,30 +440,70 @@ describe('c-quantic-breadcrumb-manager', () => { expect(labels).toEqual([values.join(' / ')]); }); }); + + describe('with a custom category divider', () => { + it('should display the breadcrumb values with the custom divider', async () => { + const customCategoryDivider = '*'; + const element = createTestComponent({ + categoryDivider: customCategoryDivider, + }); + await flushPromises(); + + const categoryFacetBreadcrumbs = element.shadowRoot.querySelectorAll( + selectors.categoryFacetBreadcrumb + ); + + expect(categoryFacetBreadcrumbs.length).toBe( + testCategoryFacetBreadcrumbs.length + ); + + categoryFacetBreadcrumbs.forEach((categoryFacetBreadcrumb, index) => { + const exampleFacetBreadcrumb = testCategoryFacetBreadcrumbs[index]; + const categoryFacetBreadcrumbValues = Array.from( + categoryFacetBreadcrumb.querySelectorAll( + selectors.categoryFacetBreadcrumbValue + ) + ); + + expect(categoryFacetBreadcrumbValues.length).toBe(1); + + const labels = categoryFacetBreadcrumbValues.map( + (facetBreadcrumbValue) => facetBreadcrumbValue.label + ); + const values = exampleFacetBreadcrumb.path.map((item) => item.value); + expect(labels).toEqual([values.join(` ${customCategoryDivider} `)]); + }); + }); + }); }); describe('numeric facet breadcrumbs', () => { - const exampleNumericFacetBreadcrumbs = [ + const testNumericFacetBreadcrumbs = [ { field: 'fieldOne', - facetId: exampleFacetId, + facetId: 'facetIdOne', values: [{value: {start: 0, end: 1}}, {value: {start: 1, end: 2}}], }, + { + field: 'fieldTwo', + facetId: 'facetIdTwo', + values: [{value: {start: 10, end: 11}}, {value: {start: 20, end: 21}}], + }, ]; beforeAll(() => { breadcrumbManagerState = { ...breadcrumbManagerState, - numericFacetBreadcrumbs: exampleNumericFacetBreadcrumbs, + numericFacetBreadcrumbs: testNumericFacetBreadcrumbs, hasBreadcrumbs: true, }; - - storeState = { - [exampleFacetId]: { - facetId: exampleFacetId, + storeState = testNumericFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, format: (item) => `${item.start} - ${item.end}`, - }, - }; + }; + return acc; + }, {}); }); it('should properly display the breadcrumb values', async () => { @@ -252,10 +515,10 @@ describe('c-quantic-breadcrumb-manager', () => { ); expect(numericFacetBreadcrumbs.length).toBe( - exampleNumericFacetBreadcrumbs.length + testNumericFacetBreadcrumbs.length ); numericFacetBreadcrumbs.forEach((numericFacetBreadcrumb, index) => { - const exampleFacetBreadcrumb = exampleNumericFacetBreadcrumbs[index]; + const exampleFacetBreadcrumb = testNumericFacetBreadcrumbs[index]; const facetBreadcrumbValues = Array.from( numericFacetBreadcrumb.querySelectorAll( selectors.numericFacetBreadcrumbValue @@ -278,30 +541,38 @@ describe('c-quantic-breadcrumb-manager', () => { }); describe('date facet breadcrumbs', () => { - const exampleDateFacetBreadcrumbs = [ + const testDateFacetBreadcrumbs = [ { field: 'fieldOne', - facetId: exampleFacetId, + facetId: 'facetIdOne', values: [ {value: {start: 'yesterday', end: 'today'}}, {value: {start: 'today', end: 'tomorrow'}}, ], }, + { + field: 'fieldTwo', + facetId: 'facetIdTwo', + values: [ + {value: {start: 'last week', end: 'today'}}, + {value: {start: 'tomorrow', end: 'next week'}}, + ], + }, ]; beforeAll(() => { breadcrumbManagerState = { ...breadcrumbManagerState, - dateFacetBreadcrumbs: exampleDateFacetBreadcrumbs, + dateFacetBreadcrumbs: testDateFacetBreadcrumbs, hasBreadcrumbs: true, }; - - storeState = { - [exampleFacetId]: { - facetId: exampleFacetId, + storeState = testDateFacetBreadcrumbs.reduce((acc, breadcrumb) => { + acc[breadcrumb.facetId] = { + facetId: breadcrumb.facetId, format: (item) => `${item.start} - ${item.end}`, - }, - }; + }; + return acc; + }, {}); }); it('should properly display the breadcrumb values', async () => { @@ -312,12 +583,10 @@ describe('c-quantic-breadcrumb-manager', () => { selectors.dateFacetBreadcrumb ); - expect(dateFacetBreadcrumbs.length).toBe( - exampleDateFacetBreadcrumbs.length - ); + expect(dateFacetBreadcrumbs.length).toBe(testDateFacetBreadcrumbs.length); dateFacetBreadcrumbs.forEach((dateFacetBreadcrumb, index) => { - const exampleFacetBreadcrumb = exampleDateFacetBreadcrumbs[index]; + const exampleFacetBreadcrumb = testDateFacetBreadcrumbs[index]; const facetBreadcrumbValues = Array.from( dateFacetBreadcrumb.querySelectorAll( selectors.dateFacetBreadcrumbValue diff --git a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/fixture.ts b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/fixture.ts new file mode 100644 index 00000000000..fb28a695457 --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/fixture.ts @@ -0,0 +1,76 @@ +import {BreadcrumbManagerObject} from './pageObject'; +import {quanticBase} from '../../../../../../playwright/fixtures/baseFixture'; +import {SearchObject} from '../../../../../../playwright/page-object/searchObject'; +import { + searchRequestRegex, + insightSearchRequestRegex, +} from '../../../../../../playwright/utils/requests'; +import {InsightSetupObject} from '../../../../../../playwright/page-object/insightSetupObject'; +import {useCaseEnum} from '../../../../../../playwright/utils/useCase'; + +const breadcrumbManagerUrl = 's/quantic-breadcrumb-manager'; + +interface BreadcrumbManagerOptions { + categoryDivider: string; + collapseThreshold: number; +} + +type QuanticBreadcrumbManagerE2EFixtures = { + breadcrumbManager: BreadcrumbManagerObject; + search: SearchObject; + options: Partial; +}; + +type QuanticBreadcrumbManagerE2ESearchFixtures = + QuanticBreadcrumbManagerE2EFixtures & { + urlHash: string; + }; + +type QuanticBreadcrumbManagerE2eInsightFixtures = + QuanticBreadcrumbManagerE2ESearchFixtures & { + insightSetup: InsightSetupObject; + }; + +export const testSearch = + quanticBase.extend({ + options: {}, + urlHash: '', + search: async ({page}, use) => { + await use(new SearchObject(page, searchRequestRegex)); + }, + breadcrumbManager: async ( + {page, options, configuration, search, urlHash}, + use + ) => { + await page.goto( + urlHash ? `${breadcrumbManagerUrl}#${urlHash}` : breadcrumbManagerUrl + ); + configuration.configure(options); + await search.waitForSearchResponse(); + await use(new BreadcrumbManagerObject(page)); + }, + }); + +export const testInsight = + quanticBase.extend({ + options: {}, + search: async ({page}, use) => { + await use(new SearchObject(page, insightSearchRequestRegex)); + }, + insightSetup: async ({page}, use) => { + await use(new InsightSetupObject(page)); + }, + breadcrumbManager: async ( + {page, options, search, configuration, insightSetup}, + use + ) => { + await page.goto(breadcrumbManagerUrl); + configuration.configure({...options, useCase: useCaseEnum.insight}); + await insightSetup.waitForInsightInterfaceInitialization(); + await search.performSearch(); + await search.waitForSearchResponse(); + await use(new BreadcrumbManagerObject(page)); + }, + }); + +export {expect} from '@playwright/test'; diff --git a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/pageObject.ts b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/pageObject.ts new file mode 100644 index 00000000000..d4bb658bd5e --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/pageObject.ts @@ -0,0 +1,282 @@ +import type {Locator, Page, Request} from '@playwright/test'; +import {isUaSearchEvent} from '../../../../../../playwright/utils/requests'; + +const breadcrumbElementsSelectors = { + regularFacet: { + component: 'c-quantic-facet', + facetValueComponent: 'c-quantic-facet-value', + breadcrumbElementTestId: 'facet-breadcrumb', + breadcrumbValueElementTestId: 'facet-breadcrumb-value', + }, + numericFacet: { + component: 'c-quantic-numeric-facet', + facetValueComponent: 'c-quantic-facet-value', + breadcrumbElementTestId: 'numeric-facet-breadcrumb', + breadcrumbValueElementTestId: 'numeric-facet-breadcrumb-value', + }, + categoryFacet: { + component: 'c-quantic-category-facet', + facetValueComponent: 'c-quantic-category-facet-value', + breadcrumbElementTestId: 'category-facet-breadcrumb', + breadcrumbValueElementTestId: 'category-facet-breadcrumb-value', + }, + timeframeFacet: { + component: 'c-quantic-timeframe-facet', + facetValueComponent: 'c-quantic-facet-value', + breadcrumbElementTestId: 'date-facet-breadcrumb', + breadcrumbValueElementTestId: 'date-facet-breadcrumb-value', + }, +}; + +export class BreadcrumbManagerObject { + constructor(public page: Page) { + this.page = page; + } + + async countAllFacetsBreadcrumb() { + return ( + (await this.allRegularFacetBreadcrumb.count()) + + (await this.allNumericFacetBreadcrumb.count()) + + (await this.allCategoryFacetBreadcrumb.count()) + + (await this.allTimeframeFacetBreadcrumb.count()) + ); + } + + /** REGULAR FACET */ + get allRegularFacetBreadcrumb(): Locator { + return this.page.getByTestId( + breadcrumbElementsSelectors.regularFacet.breadcrumbElementTestId + ); + } + + get firstRegularFacetBreadcrumb(): Locator { + return this.page + .getByTestId( + breadcrumbElementsSelectors.regularFacet.breadcrumbElementTestId + ) + .first(); + } + + async clickFirstRegularFacetBreadcrumbValue(): Promise { + await this.firstRegularFacetBreadcrumb + .getByTestId( + breadcrumbElementsSelectors.regularFacet.breadcrumbValueElementTestId + ) + .click(); + } + + get firstRegularFacet(): Locator { + return this.page + .locator(breadcrumbElementsSelectors.regularFacet.component) + .first(); + } + + get firstRegularFacetValue(): Promise { + return this.firstRegularFacet + .locator(breadcrumbElementsSelectors.regularFacet.facetValueComponent) + .first() + .locator('.facet__value-text') + .textContent(); + } + + async clickFirstRegularFacetLink(): Promise { + await this.clickFirstFacetLink(this.firstRegularFacet); + } + + async clickFirstFacetLink(facetLocator: Locator): Promise { + await facetLocator + .locator(breadcrumbElementsSelectors.regularFacet.facetValueComponent) + .first() + .click(); + } + + /** NUMERIC FACET */ + get allNumericFacetBreadcrumb(): Locator { + return this.page.getByTestId( + breadcrumbElementsSelectors.numericFacet.breadcrumbElementTestId + ); + } + + get firstNumericFacetBreadcrumb(): Locator { + return this.page + .getByTestId( + breadcrumbElementsSelectors.numericFacet.breadcrumbElementTestId + ) + .first(); + } + + async clickFirstNumericFacetBreadcrumbValue(): Promise { + await this.firstNumericFacetBreadcrumb + .getByTestId( + breadcrumbElementsSelectors.numericFacet.breadcrumbValueElementTestId + ) + .click(); + } + + get firstNumericFacet(): Locator { + return this.page + .locator(breadcrumbElementsSelectors.numericFacet.component) + .first(); + } + + get firstNumericFacetValue(): Promise { + return this.firstNumericFacet + .locator(breadcrumbElementsSelectors.numericFacet.facetValueComponent) + .first() + .locator('.facet__value-text') + .textContent(); + } + + async clickFirstNumericFacetLink(): Promise { + await this.clickFirstFacetLink(this.firstNumericFacet); + } + + /** DATE|TIMEFRAME FACET */ + get allTimeframeFacetBreadcrumb(): Locator { + return this.page.getByTestId( + breadcrumbElementsSelectors.timeframeFacet.breadcrumbElementTestId + ); + } + + get firstTimeframeFacetBreadcrumb(): Locator { + return this.page + .getByTestId( + breadcrumbElementsSelectors.timeframeFacet.breadcrumbElementTestId + ) + .first(); + } + + async clickFirstTimeframeFacetBreadcrumbValue(): Promise { + await this.firstTimeframeFacetBreadcrumb + .getByTestId( + breadcrumbElementsSelectors.timeframeFacet.breadcrumbValueElementTestId + ) + .click(); + } + + get firstTimeframeFacet(): Locator { + return this.page + .locator(breadcrumbElementsSelectors.timeframeFacet.component) + .first(); + } + + get firstTimeframeFacetValue(): Promise { + return this.firstTimeframeFacet + .locator(breadcrumbElementsSelectors.timeframeFacet.facetValueComponent) + .first() + .locator('.facet__value-text') + .textContent(); + } + + async clickFirstTimeframeFacetLink(): Promise { + await this.clickFirstFacetLink(this.firstTimeframeFacet); + } + + /** CATEGORY FACET */ + get allCategoryFacetBreadcrumb(): Locator { + return this.page.getByTestId( + breadcrumbElementsSelectors.categoryFacet.breadcrumbElementTestId + ); + } + + get firstCategoryFacetBreadcrumb(): Locator { + return this.page + .getByTestId( + breadcrumbElementsSelectors.categoryFacet.breadcrumbElementTestId + ) + .first(); + } + + async clickFirstCategoryFacetBreadcrumbValue(): Promise { + await this.firstCategoryFacetBreadcrumb + .getByTestId( + breadcrumbElementsSelectors.categoryFacet.breadcrumbValueElementTestId + ) + .click(); + } + + get firstCategoryFacet(): Locator { + return this.page + .locator(breadcrumbElementsSelectors.categoryFacet.component) + .first(); + } + + get firstCategoryFacetValue(): Promise { + return this.firstCategoryFacet + .locator(breadcrumbElementsSelectors.categoryFacet.facetValueComponent) + .first() + .locator('.facet__value-option') + .locator('span:not(.facet__number-of-results)') + .textContent(); + } + + async clickFirstCategoryFacetLink(): Promise { + await this.clickFirstCategoryFacetValue(this.firstCategoryFacet); + } + + async clickFirstCategoryFacetValue(facetLocator: Locator): Promise { + await facetLocator + .locator(breadcrumbElementsSelectors.categoryFacet.facetValueComponent) + .first() + .click(); + } + + get clearAllButton(): Locator { + return this.page.getByRole('button', {name: /Clear All Filters/i}); + } + + async clickClearAllButton(): Promise { + await this.clearAllButton.click(); + } + + async waitForBreadcrumbSearchUaAnalytics( + actionCause: string, + customChecker?: Function + ): Promise { + const uaRequest = this.page.waitForRequest((request) => { + if (isUaSearchEvent(request)) { + const requestBody = request.postDataJSON?.(); + const {customData} = requestBody; + + const expectedFields = { + actionCause: actionCause, + }; + const matchesExpectedFields = Object.keys(expectedFields).every( + (key) => requestBody?.[key] === expectedFields[key] + ); + + return ( + matchesExpectedFields && + (customChecker ? customChecker(customData) : true) + ); + } + return false; + }); + return uaRequest; + } + + async waitForBreadcrumbFacetUaAnalytics( + expectedCustomFields: Record + ): Promise { + return this.waitForBreadcrumbSearchUaAnalytics( + 'breadcrumbFacet', + (customData: Record) => { + return Object.keys(expectedCustomFields).every((key) => + typeof expectedCustomFields[key] === 'object' + ? JSON.stringify(customData?.[key]) === + JSON.stringify(expectedCustomFields[key]) + : customData?.[key] === expectedCustomFields[key] + ); + } + ); + } + + async waitForBreadcrumbResetAllUaAnalytics( + customChecker?: Function + ): Promise { + return this.waitForBreadcrumbSearchUaAnalytics( + 'breadcrumbResetAll', + customChecker + ); + } +} diff --git a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/quanticBreadcrumbManager.e2e.ts b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/quanticBreadcrumbManager.e2e.ts new file mode 100644 index 00000000000..5e92bfb403c --- /dev/null +++ b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/e2e/quanticBreadcrumbManager.e2e.ts @@ -0,0 +1,167 @@ +import {testSearch, testInsight} from './fixture'; +import { + useCaseEnum, + useCaseTestCases, +} from '../../../../../../playwright/utils/useCase'; + +const fixtures = { + search: testSearch, + insight: testInsight, +}; + +const datetimeFacetLabelToValue = { + 'Past week': 'past-1-week..now', + 'Past month': 'past-1-month..now', + 'Past 6 months': 'past-6-month..now', + 'Past year': 'past-1-year..now', + 'Past decade': 'past-10-year..now', +}; + +useCaseTestCases.forEach((useCase) => { + let test = fixtures[useCase.value]; + + test.describe(`quantic breadcrumb manager ${useCase.label}`, () => { + test.describe('when de-selecting a facet value', () => { + test.describe('with regular facet values', () => { + test('should trigger a new search and log analytics', async ({ + breadcrumbManager, + search, + }) => { + const firstRegularFacetValue = + await breadcrumbManager.firstRegularFacetValue; + const searchResponsePromise = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstRegularFacetLink(); + await searchResponsePromise; + + const secondSearchResponsePromise = search.waitForSearchResponse(); + const breadcrumbAnalyticsPromise = + breadcrumbManager.waitForBreadcrumbFacetUaAnalytics({ + facetValue: firstRegularFacetValue, + }); + await breadcrumbManager.clickFirstRegularFacetBreadcrumbValue(); + await secondSearchResponsePromise; + await breadcrumbAnalyticsPromise; + }); + }); + + test.describe('with numeric facet values', () => { + test('should trigger a new search and log analytics', async ({ + breadcrumbManager, + search, + }) => { + const [facetRangeStart, facetRangeEnd] = + (await breadcrumbManager.firstNumericFacetValue)?.split(' - ') ?? + []; + const searchResponsePromise = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstNumericFacetLink(); + await searchResponsePromise; + + const secondSearchResponsePromise = search.waitForSearchResponse(); + // We use the facet display value to compare, but the display value is localized and can contain non-numeric characters while the analytics request doesn't contain those characters. + const breadcrumbAnalyticsPromise = + breadcrumbManager.waitForBreadcrumbFacetUaAnalytics({ + facetRangeStart: facetRangeStart.replace(/[^\d]/g, ''), + facetRangeEnd: facetRangeEnd.replace(/[^\d]/g, ''), + }); + await breadcrumbManager.clickFirstNumericFacetBreadcrumbValue(); + await secondSearchResponsePromise; + await breadcrumbAnalyticsPromise; + }); + }); + + test.describe('with timeframe|date facet values', () => { + test('should trigger a new search and log analytics', async ({ + breadcrumbManager, + search, + }) => { + const facetValue = await breadcrumbManager.firstTimeframeFacetValue; + test.expect(facetValue).not.toBeNull(); + const searchResponsePromise = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstTimeframeFacetLink(); + await searchResponsePromise; + + const secondSearchResponsePromise = search.waitForSearchResponse(); + // We use the facet display value to compare, but the display value is prettified and is transformed to be human readable, while the analytics request isn't. + const [expectedFacetRangeStart, expectedFacetRangeEnd] = + datetimeFacetLabelToValue[facetValue!].split('..'); + const breadcrumbAnalyticsPromise = + breadcrumbManager.waitForBreadcrumbFacetUaAnalytics({ + facetRangeStart: expectedFacetRangeStart, + facetRangeEnd: expectedFacetRangeEnd, + }); + await breadcrumbManager.clickFirstTimeframeFacetBreadcrumbValue(); + await secondSearchResponsePromise; + await breadcrumbAnalyticsPromise; + }); + }); + + test.describe('with category facet values', () => { + test('should trigger a new search and log analytics', async ({ + breadcrumbManager, + search, + }) => { + const facetValue = await breadcrumbManager.firstCategoryFacetValue; + test.expect(facetValue).not.toBeNull(); + const searchResponsePromise = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstCategoryFacetLink(); + await searchResponsePromise; + + const secondSearchResponsePromise = search.waitForSearchResponse(); + const expectedFacetValue = facetValue!; + const breadcrumbAnalyticsPromise = + breadcrumbManager.waitForBreadcrumbFacetUaAnalytics({ + categoryFacetPath: [expectedFacetValue], + }); + await breadcrumbManager.clickFirstCategoryFacetBreadcrumbValue(); + await secondSearchResponsePromise; + await breadcrumbAnalyticsPromise; + }); + }); + }); + + test.describe('when clicking on the clear all filters button', () => { + test('should send a clear all filters analytics event and clear all selected facets values', async ({ + breadcrumbManager, + search, + }) => { + const searchResponsePromise = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstRegularFacetLink(); + await searchResponsePromise; + + const searchResponsePromiseTwo = search.waitForSearchResponse(); + await breadcrumbManager.clickFirstTimeframeFacetLink(); + await searchResponsePromiseTwo; + + test.expect(breadcrumbManager.clearAllButton).toBeVisible(); + const clearAllSearchResponsePromise = search.waitForSearchResponse(); + const clearAllAnalyticsPromise = + breadcrumbManager.waitForBreadcrumbResetAllUaAnalytics(); + await breadcrumbManager.clickClearAllButton(); + await clearAllAnalyticsPromise; + const clearAllSearchResponse = await clearAllSearchResponsePromise; + const {facets} = await clearAllSearchResponse.json(); + const activeFacets = facets.filter((facet: any) => + facet.values.some((value: any) => value.state !== 'idle') + ); + test.expect(activeFacets).toHaveLength(0); + }); + }); + + if (useCase.value === useCaseEnum.search) { + test.describe('when loading with a facet already selected', () => { + const facetSelectedHash = 'f-filetype=YouTubeVideo'; + test.use({ + urlHash: facetSelectedHash, + }); + + test('should have a facet selected in the breadcrumb manager', async ({ + breadcrumbManager, + }) => { + test + .expect(await breadcrumbManager.countAllFacetsBreadcrumb()) + .toBe(1); + }); + }); + } + }); +}); diff --git a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/quanticBreadcrumbManager.html b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/quanticBreadcrumbManager.html index 32c7eacbd6b..ee204e37ef2 100644 --- a/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/quanticBreadcrumbManager.html +++ b/packages/quantic/force-app/main/default/lwc/quanticBreadcrumbManager/quanticBreadcrumbManager.html @@ -9,29 +9,29 @@