diff --git a/packages/atomic-react/src/components/search/components.ts b/packages/atomic-react/src/components/search/components.ts index ea375881b7f..18dafe4847a 100644 --- a/packages/atomic-react/src/components/search/components.ts +++ b/packages/atomic-react/src/components/search/components.ts @@ -1,5 +1,6 @@ import { AtomicAriaLive as LitAtomicAriaLive, + AtomicAutomaticFacet as LitAtomicAutomaticFacet, AtomicComponentError as LitAtomicComponentError, AtomicExternal as LitAtomicExternal, AtomicFacet as LitAtomicFacet, @@ -49,6 +50,12 @@ export const AtomicAriaLive = createComponent({ elementClass: LitAtomicAriaLive, }); +export const AtomicAutomaticFacet = createComponent({ + tagName: 'atomic-automatic-facet', + react: React, + elementClass: LitAtomicAutomaticFacet, +}); + export const AtomicComponentError = createComponent({ tagName: 'atomic-component-error', react: React, diff --git a/packages/atomic/cypress/e2e/breadbox.cypress.ts b/packages/atomic/cypress/e2e/breadbox.cypress.ts index 1c50ff01070..2246f6c7868 100644 --- a/packages/atomic/cypress/e2e/breadbox.cypress.ts +++ b/packages/atomic/cypress/e2e/breadbox.cypress.ts @@ -7,8 +7,6 @@ import { import * as BreadboxAssertions from './breadbox-assertions'; import {breadboxComponent, BreadboxSelectors} from './breadbox-selectors'; import * as CommonAssertions from './common-assertions'; -import {addAutomaticFacetGenerator} from './facets/automatic-facet-generator/automatic-facet-generator-actions'; -import {AutomaticFacetSelectors} from './facets/automatic-facet/automatic-facet-selectors'; import { addCategoryFacet, canadaHierarchy, @@ -59,64 +57,6 @@ describe('Breadbox Test Suites', () => { .init(); } - // When an automatic facet generator is used with other facets, if the query is too narrow, there won't be any automatic facet. - describe('when selecting an automatic facet', () => { - const selectionIndex = 2; - function setupBreadboxWithMultipleSelectedFacets() { - new TestFixture() - .withTranslation({'a.translated.label': 'This is a translated label'}) - .with(addBreadbox()) - .with( - addAutomaticFacetGenerator({ - 'desired-count': '1', - }) - ) - .init(); - selectIdleCheckboxValueAt(AutomaticFacetSelectors, selectionIndex); - } - - describe('verify rendering', () => { - beforeEach(() => setupBreadboxWithMultipleSelectedFacets()); - BreadboxAssertions.assertDisplayBreadcrumb(true); - CommonAssertions.assertAccessibility(breadboxComponent); - BreadboxAssertions.assertDisplayBreadcrumbClearAllButton(true); - BreadboxAssertions.assertBreadcrumbLabel(breadboxLabel); - it('should display the selected checkbox facets in the breadcrumbs', () => { - AutomaticFacetSelectors.labelButton() - .invoke('text') - .then((facetLabel) => { - BreadboxAssertions.assertSelectedCheckboxFacetsInBreadcrumbAssertions( - AutomaticFacetSelectors, - facetLabel - ); - }); - }); - BreadboxAssertions.assertDisplayBreadcrumbClearIcon(); - BreadboxAssertions.assertBreadcrumbDisplayLength(1); - }); - - describe('when selecting "Clear all" button', () => { - function setupClearAllBreadcrumb() { - setupBreadboxWithMultipleSelectedFacets(); - deselectAllBreadcrumbs(); - } - - describe('verify rendering', () => { - beforeEach(setupClearAllBreadcrumb); - BreadboxAssertions.assertDisplayBreadcrumb(false); - CommonFacetAssertions.assertNumberOfSelectedCheckboxValues( - AutomaticFacetSelectors, - 0 - ); - }); - - describe('verify analytics', () => { - beforeEach(setupClearAllBreadcrumb); - BreadboxAssertions.assertLogBreadcrumbClearAll(); - }); - }); - }); - describe('when selecting a standard facet, a numeric facet', () => { const selectionIndex = 2; function setupBreadboxWithMultipleSelectedFacets(props: TagProps = {}) { diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-actions.ts b/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-actions.ts deleted file mode 100644 index a37571010c2..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-actions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - TagProps, - generateComponentHTML, -} from '../../../fixtures/fixture-common'; -import {TestFixture} from '../../../fixtures/test-fixture'; -import {automaticFacetGeneratorComponent} from './automatic-facet-generator-assertions'; - -export const addAutomaticFacetGenerator = - (props: TagProps = {}) => - (env: TestFixture) => { - const automaticFacetGenerator = generateComponentHTML( - automaticFacetGeneratorComponent, - props - ); - - env.withElement(automaticFacetGenerator); - }; diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-assertions.ts b/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-assertions.ts deleted file mode 100644 index 1b5d4445fea..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator-assertions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {automaticFacetComponent} from '../automatic-facet/automatic-facet-selectors'; - -export const automaticFacetGeneratorComponent = - 'atomic-automatic-facet-generator'; - -export function assertContainsAutomaticFacet() { - cy.get(automaticFacetGeneratorComponent).find(automaticFacetComponent); -} - -export function assertDisplayPlaceholder() { - cy.get(automaticFacetGeneratorComponent).find('[part="placeholder"]'); -} - -export function assertDisplayNothing() { - cy.get(automaticFacetGeneratorComponent).children().should('not.exist'); -} - -export function assertDesiredCountIsDefaultValue() { - cy.get(automaticFacetGeneratorComponent) - .invoke('attr', 'desired-count') - .should('eq', '5'); -} - -export function assertNumberOfValuesIsDefaultValue() { - cy.get(automaticFacetGeneratorComponent) - .invoke('attr', 'number-of-values') - .should('eq', '8'); -} diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator.cypress.ts b/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator.cypress.ts deleted file mode 100644 index 061475e45e5..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet-generator/automatic-facet-generator.cypress.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {TestFixture} from '../../../fixtures/test-fixture'; -import { - assertConsoleError, - assertContainsComponentError, -} from '../../common-assertions'; -import {addAutomaticFacetGenerator} from './automatic-facet-generator-actions'; -import { - assertContainsAutomaticFacet, - assertDesiredCountIsDefaultValue, - assertDisplayNothing, - assertDisplayPlaceholder, - assertNumberOfValuesIsDefaultValue, - automaticFacetGeneratorComponent, -} from './automatic-facet-generator-assertions'; - -describe('Automatic Facet Generator Test Suites', () => { - it('should throw an error when desiredCount property is invalid', () => { - new TestFixture() - .with(addAutomaticFacetGenerator({'desired-count': 'potato'})) - .init(); - assertConsoleError(); - assertContainsComponentError( - { - shadow: () => cy.get(automaticFacetGeneratorComponent).shadow(), - }, - true - ); - }); - - it('should display atomic-automatic-facet when desiredCount is valid', () => { - new TestFixture() - .with(addAutomaticFacetGenerator({'desired-count': '1'})) - .init(); - assertContainsAutomaticFacet(); - }); - - it('should throw an error when areCollapsed property is invalid', () => { - new TestFixture() - .with(addAutomaticFacetGenerator({'are-collapsed': 'potato'})) - .init(); - assertConsoleError(); - assertContainsComponentError( - { - shadow: () => cy.get(automaticFacetGeneratorComponent).shadow(), - }, - true - ); - }); - - it('should display placeholders when no search has yet been executed', () => { - new TestFixture() - .with( - addAutomaticFacetGenerator({ - 'desired-count': '1', - }) - ) - .withoutFirstAutomaticSearch() - .init(); - assertDisplayPlaceholder(); - }); - - it('should display nothing when response is empty', () => { - new TestFixture() - .with( - addAutomaticFacetGenerator({ - 'desired-count': '1', - }) - ) - .withoutAutomaticFacets() - .init(); - assertDisplayNothing(); - }); - - it('should display atomic-automatic-facet when props are empty', () => { - new TestFixture().with(addAutomaticFacetGenerator({})).init(); - - assertContainsAutomaticFacet(); - }); - - it('should have the default values when props are empty', () => { - new TestFixture().with(addAutomaticFacetGenerator({})).init(); - - assertDesiredCountIsDefaultValue(); - assertNumberOfValuesIsDefaultValue(); - }); -}); diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-assertions.ts b/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-assertions.ts deleted file mode 100644 index 1575d2dc990..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-assertions.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - BaseFacetSelector, - FacetWithCheckboxSelector, -} from '../facet-common-assertions'; -import {automaticFacetComponent} from './automatic-facet-selectors'; - -export function assertLabelIsNotEmpty(BaseFacetSelector: BaseFacetSelector) { - it('should have a label', () => { - BaseFacetSelector.labelButton().should('not.be.empty'); - }); -} - -export function assertValueAtIndex( - FacetWithCheckboxSelector: FacetWithCheckboxSelector & BaseFacetSelector, - index: number -) { - it(`should go to index ${index}`, () => { - FacetWithCheckboxSelector.selectedCheckboxValue().eq(index); - }); -} - -export function assertLogFacetSelect() { - it('should log the facet select results to UA ', () => { - cy.expectSearchEvent('facetSelect').then((analyticsBody) => { - cy.get(automaticFacetComponent) - .invoke('attr', 'field') - .then((value) => { - expect(analyticsBody.customData).to.have.property( - 'facetField', - value - ); - expect(analyticsBody.facetState![0]).to.have.property('field', value); - }); - expect(analyticsBody.facetState![0]).to.have.property( - 'state', - 'selected' - ); - }); - }); -} - -export function assertLogClearFacetValues() { - it('should log the facet clear all to UA', () => { - cy.expectSearchEvent('facetClearAll').then((analyticsBody) => { - cy.get(automaticFacetComponent) - .invoke('attr', 'field') - .then((value) => { - expect(analyticsBody.customData).to.have.property( - 'facetField', - value - ); - }); - }); - }); -} - -export function assertLabel( - BaseFacetSelector: BaseFacetSelector, - labelValue: string, - fieldOrLabel: string -) { - it(`should have the ${fieldOrLabel} value as the value`, () => { - BaseFacetSelector.labelButton().contains(labelValue); - }); -} diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-selectors.ts b/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-selectors.ts deleted file mode 100644 index 57940ec9bda..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet-selectors.ts +++ /dev/null @@ -1,70 +0,0 @@ -export const automaticFacetComponent = 'atomic-automatic-facet'; - -export const AutomaticFacetSelectors = { - shadow() { - return cy.get(automaticFacetComponent).shadow(); - }, - placeholder() { - return this.shadow().find('[part="placeholder"]'); - }, - selectedCheckboxValue() { - return this.shadow().find( - '[part~="value-checkbox"][part~="value-checkbox-checked"][aria-checked="true"]' - ); - }, - idleCheckboxValue() { - return this.shadow().find( - '[part~="value-checkbox"]:not([part~="value-checkbox-checked"])[aria-checked="false"]' - ); - }, - checkboxValueWithText(text: string) { - return this.shadow() - .find('[part="value-label"]') - .contains(text) - .parent() - .parent() - .find('[part~="value-checkbox"]'); - }, - idleCheckboxValueLabel() { - return this.idleCheckboxValue().parent().find('[part="value-label"]'); - }, - wrapper() { - return this.shadow().find('[part="facet"]'); - }, - labelButton() { - return this.shadow().find('[part="label-button"]'); - }, - labelButtonIcon() { - return this.shadow().find('[part="label-button-icon"]', {timeout: 8000}); - }, - clearButton() { - return this.shadow().find('[part="clear-button"]'); - }, - clearButtonIcon() { - return this.shadow().find('[part="clear-button-icon"]', {timeout: 8000}); - }, - values() { - return this.shadow().find('[part="values"]'); - }, - valueLabel() { - return this.shadow().find('[part="value-label"]'); - }, - valueCount() { - return this.shadow().find('[part="value-count"]'); - }, - valueCheckbox() { - return this.shadow().find('[part="value-checkbox"]'); - }, - valueCheckboxChecked() { - return this.shadow().find('[part="value-checkbox-checked"]'); - }, - valueCheckboxLabel() { - return this.shadow().find('[part="value-checkbox-label"]'); - }, - valueCheckboxIcon() { - return this.shadow().find('[part="value-checkbox-icon"]', {timeout: 8000}); - }, - facetValueLabelAtIndex(index: number) { - return this.valueLabel().eq(index); - }, -}; diff --git a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet.cypress.ts b/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet.cypress.ts deleted file mode 100644 index 81b694460c3..00000000000 --- a/packages/atomic/cypress/e2e/facets/automatic-facet/automatic-facet.cypress.ts +++ /dev/null @@ -1,177 +0,0 @@ -import {TestFixture} from '../../../fixtures/test-fixture'; -import * as CommonAssertions from '../../common-assertions'; -import {addAutomaticFacetGenerator} from '../automatic-facet-generator/automatic-facet-generator-actions'; -import * as AutomaticFacetAssertions from '../automatic-facet/automatic-facet-assertions'; -import { - pressClearButton, - selectIdleCheckboxValueAt, -} from '../facet-common-actions'; -import * as CommonFacetAssertions from '../facet-common-assertions'; -import { - AutomaticFacetSelectors, - automaticFacetComponent, -} from './automatic-facet-selectors'; - -describe('Automatic Facet Test Suites', () => { - function setup() { - new TestFixture() - .with( - addAutomaticFacetGenerator({ - 'desired-count': '1', - 'are-collapsed': 'false', - }) - ) - .init(); - } - - describe('verify rendering', () => { - beforeEach(setup); - - // CommonAssertions.assertAccessibility(automaticFacetComponent); - CommonAssertions.assertContainsComponentError( - AutomaticFacetSelectors, - false - ); - CommonFacetAssertions.assertDisplayFacet(AutomaticFacetSelectors, true); - AutomaticFacetAssertions.assertLabelIsNotEmpty(AutomaticFacetSelectors); - CommonFacetAssertions.assertDisplayValues(AutomaticFacetSelectors, true); - CommonFacetAssertions.assertNumberOfSelectedCheckboxValues( - AutomaticFacetSelectors, - 0 - ); - CommonFacetAssertions.assertDisplayClearButton( - AutomaticFacetSelectors, - false - ); - }); - - describe('verify label', () => { - const fieldName = 'field'; - function setupForLabelLogic(responseLabel: string | undefined) { - new TestFixture() - .with( - addAutomaticFacetGenerator({ - 'desired-count': '1', - 'are-collapsed': 'false', - }) - ) - .withCustomResponse((r) => { - r.generateAutomaticFacets = { - facets: [ - { - field: fieldName, - label: responseLabel, - values: [], - }, - ], - }; - }) - .init(); - } - - describe('when it is defined in the response', () => { - beforeEach(() => setupForLabelLogic('label')); - - AutomaticFacetAssertions.assertLabel( - AutomaticFacetSelectors, - 'label', - 'label' - ); - }); - - describe('when it is undefined in the response', () => { - beforeEach(() => setupForLabelLogic(undefined)); - - AutomaticFacetAssertions.assertLabel( - AutomaticFacetSelectors, - fieldName, - 'field' - ); - }); - }); - - describe('when selecting a value', () => { - const index = 1; - function setupSelectValue() { - setup(); - selectIdleCheckboxValueAt(AutomaticFacetSelectors, index); - } - - describe('verify rendering', () => { - beforeEach(setupSelectValue); - - // CommonAssertions.assertAccessibility(automaticFacetComponent); - CommonFacetAssertions.assertDisplayClearButton( - AutomaticFacetSelectors, - true - ); - CommonFacetAssertions.assertNumberOfSelectedCheckboxValues( - AutomaticFacetSelectors, - 1 - ); - AutomaticFacetAssertions.assertValueAtIndex(AutomaticFacetSelectors, 0); - }); - - describe('verify analytics', () => { - beforeEach(setupSelectValue); - - AutomaticFacetAssertions.assertLogFacetSelect(); - }); - - describe('when selecting a second value', () => { - const index = 2; - function setupSelectSecondValue() { - setupSelectValue(); - selectIdleCheckboxValueAt(AutomaticFacetSelectors, index); - } - - describe('verify rendering', () => { - beforeEach(setupSelectSecondValue); - - // CommonAssertions.assertAccessibility(automaticFacetComponent); - CommonFacetAssertions.assertDisplayClearButton( - AutomaticFacetSelectors, - true - ); - CommonFacetAssertions.assertNumberOfSelectedCheckboxValues( - AutomaticFacetSelectors, - 2 - ); - AutomaticFacetAssertions.assertValueAtIndex(AutomaticFacetSelectors, 1); - }); - - describe('verify analytics', () => { - beforeEach(setupSelectSecondValue); - - AutomaticFacetAssertions.assertLogFacetSelect(); - }); - - describe('when selecting the "Clear" button', () => { - function setUpClearValues() { - setupSelectSecondValue(); - pressClearButton(AutomaticFacetSelectors); - } - - describe('verify rendering', () => { - beforeEach(setUpClearValues); - - CommonFacetAssertions.assertDisplayClearButton( - AutomaticFacetSelectors, - false - ); - CommonFacetAssertions.assertNumberOfSelectedCheckboxValues( - AutomaticFacetSelectors, - 0 - ); - CommonFacetAssertions.assertFocusHeader(AutomaticFacetSelectors); - }); - - describe('verify analytics', () => { - beforeEach(setUpClearValues); - - AutomaticFacetAssertions.assertLogClearFacetValues(); - }); - }); - }); - }); -}); diff --git a/packages/atomic/cypress/e2e/facets/manager/facet-manager-actions.ts b/packages/atomic/cypress/e2e/facets/manager/facet-manager-actions.ts index 6ef3c6dc525..9a3698d8e0d 100644 --- a/packages/atomic/cypress/e2e/facets/manager/facet-manager-actions.ts +++ b/packages/atomic/cypress/e2e/facets/manager/facet-manager-actions.ts @@ -3,7 +3,6 @@ import { TagProps, TestFixture, } from '../../../fixtures/test-fixture'; -import {automaticFacetGeneratorComponent} from '../automatic-facet-generator/automatic-facet-generator-assertions'; import {hierarchicalField} from '../category-facet/category-facet-actions'; import {categoryFacetComponent} from '../category-facet/category-facet-selectors'; import {colorFacetField} from '../color-facet/color-facet-actions'; @@ -53,39 +52,3 @@ export const addFacetManagerWithStaticFacets = env.withElement(manager); }; - -export const addFacetManagerWithAutomaticFacets = - (props: TagProps = {}) => - (env: TestFixture) => { - const manager = generateComponentHTML(facetManagerComponent, props); - manager.append( - generateComponentHTML(automaticFacetGeneratorComponent, { - 'desired-count': '3', - }) - ); - - env.withElement(manager); - }; - -export const addFacetManagerWithBothTypesOfFacets = - (props: TagProps = {}, generatorCollapseFacetsAfter = 3) => - (env: TestFixture) => { - const manager = generateComponentHTML(facetManagerComponent, props); - - manager.append(generateComponentHTML(facetComponent, {field: facetField})); - manager.append( - generateComponentHTML(numericFacetComponent, {field: numericFacetField}) - ); - manager.append( - generateComponentHTML(categoryFacetComponent, {field: hierarchicalField}) - ); - - manager.append( - generateComponentHTML(automaticFacetGeneratorComponent, { - 'desired-count': '3', - collapseFacetsAfter: generatorCollapseFacetsAfter, - }) - ); - - env.withElement(manager); - }; diff --git a/packages/atomic/cypress/e2e/facets/manager/facet-manager-assertions.ts b/packages/atomic/cypress/e2e/facets/manager/facet-manager-assertions.ts index c7d5a2fba8b..a3f31596eda 100644 --- a/packages/atomic/cypress/e2e/facets/manager/facet-manager-assertions.ts +++ b/packages/atomic/cypress/e2e/facets/manager/facet-manager-assertions.ts @@ -1,4 +1,3 @@ -import {automaticFacetGeneratorComponent} from '../automatic-facet-generator/automatic-facet-generator-assertions'; import {facetManagerComponent} from './facet-manager-actions'; export function assertHasNumberOfExpandedFacets( @@ -22,16 +21,4 @@ export function assertFacetsNoCollapsedAttribute() { }); } -export function assertHasNumberOfExpandedAutomaticFacets( - numberOfExpandedAutomaticFacets: number -) { - cy.get(automaticFacetGeneratorComponent) - .children() - .each(($child, index) => { - if (index + 1 > numberOfExpandedAutomaticFacets) { - cy.wrap($child).should('have.attr', 'is-collapsed'); - return; - } - cy.wrap($child).should('not.have.attr', 'is-collapsed'); - }); -} + diff --git a/packages/atomic/cypress/e2e/facets/manager/facet-manager.cypress.ts b/packages/atomic/cypress/e2e/facets/manager/facet-manager.cypress.ts index 0e47ae7ec54..129a7e9e060 100644 --- a/packages/atomic/cypress/e2e/facets/manager/facet-manager.cypress.ts +++ b/packages/atomic/cypress/e2e/facets/manager/facet-manager.cypress.ts @@ -4,14 +4,11 @@ import { assertContainsComponentError, } from '../../common-assertions'; import { - addFacetManagerWithAutomaticFacets, - addFacetManagerWithBothTypesOfFacets, addFacetManagerWithStaticFacets, facetManagerComponent, } from './facet-manager-actions'; import { assertFacetsNoCollapsedAttribute, - assertHasNumberOfExpandedAutomaticFacets, assertHasNumberOfExpandedFacets, } from './facet-manager-assertions'; @@ -53,53 +50,4 @@ describe('Facet Manager Test Suite', () => { assertFacetsNoCollapsedAttribute(); }); }); - - describe('with automatic facets only', () => { - it('should only keep the first 4 facets expanded by default', () => { - new TestFixture().with(addFacetManagerWithAutomaticFacets()).init(); - assertHasNumberOfExpandedAutomaticFacets(4); - }); - - it('should respect the collapseFacetsAfter prop when set', () => { - new TestFixture() - .with(addFacetManagerWithAutomaticFacets({'collapse-facets-after': 1})) - .init(); - assertHasNumberOfExpandedAutomaticFacets(1); - }); - - it('should disable the collapseFacetsAfter prop when set to -1', () => { - new TestFixture() - .with(addFacetManagerWithAutomaticFacets({'collapse-facets-after': -1})) - .init(); - assertHasNumberOfExpandedAutomaticFacets(3); - }); - }); - - describe('with both types of facets', () => { - it('should only keep the first 4 facets expanded by default', () => { - new TestFixture().with(addFacetManagerWithBothTypesOfFacets()).init(); - assertHasNumberOfExpandedFacets(3); - assertHasNumberOfExpandedAutomaticFacets(1); - }); - - it('should respect the collapseFacetsAfter prop when set', () => { - new TestFixture() - .with( - addFacetManagerWithBothTypesOfFacets({'collapse-facets-after': 2}) - ) - .init(); - assertHasNumberOfExpandedFacets(2); - assertHasNumberOfExpandedAutomaticFacets(0); - }); - - it('should disable the collapseFacetsAfter prop when set to -1', () => { - new TestFixture() - .with( - addFacetManagerWithBothTypesOfFacets({'collapse-facets-after': -1}) - ) - .init(); - assertHasNumberOfExpandedAutomaticFacets(3); - assertHasNumberOfExpandedAutomaticFacets(3); - }); - }); }); diff --git a/packages/atomic/cypress/e2e/refine-toggle-actions.ts b/packages/atomic/cypress/e2e/refine-toggle-actions.ts index 3591ca6bf47..59a6ddcdf32 100644 --- a/packages/atomic/cypress/e2e/refine-toggle-actions.ts +++ b/packages/atomic/cypress/e2e/refine-toggle-actions.ts @@ -3,7 +3,6 @@ import { TagProps, TestFixture, } from '../fixtures/test-fixture'; -import {automaticFacetGeneratorComponent} from './facets/automatic-facet-generator/automatic-facet-generator-assertions'; import {hierarchicalField} from './facets/category-facet/category-facet-actions'; import {categoryFacetComponent} from './facets/category-facet/category-facet-selectors'; import {colorFacetField} from './facets/color-facet/color-facet-actions'; @@ -72,34 +71,6 @@ export const addRefineToggleWithStaticFacets = export const addRefineToggleWithStaticFacetsAndNoManager = addRefineToggleWithStaticFacetsWithSpecificParent('div'); -export const addRefineToggleWithAutomaticFacets = - (props: TagProps = {}) => - (env: TestFixture) => { - const automaticFacetGenerator = generateComponentHTML( - automaticFacetGeneratorComponent, - {'desired-count': '3'} - ); - const refineToggle = generateComponentHTML(refineToggleComponent, props); - - env.withElement(automaticFacetGenerator).withElement(refineToggle); - }; - -export const addFacetManagerWithBothTypesOfFacets = - (props: TagProps = {}) => - (env: TestFixture) => { - const manager = generateComponentHTML(facetManagerComponent); - manager.append(generateComponentHTML(facetComponent, {field: facetField})); - const automaticFacetGenerator = generateComponentHTML( - automaticFacetGeneratorComponent, - {'desired-count': '1'} - ); - const refineToggle = generateComponentHTML(refineToggleComponent, props); - - env - .withElement(manager) - .withElement(automaticFacetGenerator) - .withElement(refineToggle); - }; export const addRefineToggleRangeVariations = (props: TagProps = {}) => diff --git a/packages/atomic/cypress/e2e/refine-toggle-selectors.ts b/packages/atomic/cypress/e2e/refine-toggle-selectors.ts index 92800bac44f..ccebd4df5ca 100644 --- a/packages/atomic/cypress/e2e/refine-toggle-selectors.ts +++ b/packages/atomic/cypress/e2e/refine-toggle-selectors.ts @@ -1,4 +1,3 @@ -import {automaticFacetGeneratorComponent} from './facets/automatic-facet-generator/automatic-facet-generator-assertions'; export const refineToggleComponent = 'atomic-refine-toggle'; export const refineModalComponent = 'atomic-refine-modal'; @@ -22,11 +21,6 @@ export const RefineModalSelectors = { footerButton: () => RefineModalSelectors.shadow().find('[part="footer-button"]'), facets: () => cy.get(refineModalComponent).find('[slot="facets"]'), - automaticFacets: () => - cy - .get(refineModalComponent) - .find('[slot="facets"]') - .find(automaticFacetGeneratorComponent), filterSection: () => RefineModalSelectors.shadow().find('[part="filter-section"]'), }; diff --git a/packages/atomic/cypress/e2e/refine-toggle.cypress.ts b/packages/atomic/cypress/e2e/refine-toggle.cypress.ts index 58c0dccf802..5f4ca27c742 100644 --- a/packages/atomic/cypress/e2e/refine-toggle.cypress.ts +++ b/packages/atomic/cypress/e2e/refine-toggle.cypress.ts @@ -1,6 +1,5 @@ import {TestFixture} from '../fixtures/test-fixture'; import * as CommonAssertions from './common-assertions'; -import {automaticFacetGeneratorComponent} from './facets/automatic-facet-generator/automatic-facet-generator-assertions'; import {hierarchicalField} from './facets/category-facet/category-facet-actions'; import {categoryFacetComponent} from './facets/category-facet/category-facet-selectors'; import {colorFacetField} from './facets/color-facet/color-facet-actions'; @@ -16,9 +15,7 @@ import {ratingRangeFacetComponent} from './facets/rating-range-facet/rating-rang import {timeframeFacetField} from './facets/timeframe-facet/timeframe-facet-action'; import {timeframeFacetComponent} from './facets/timeframe-facet/timeframe-facet-selectors'; import { - addFacetManagerWithBothTypesOfFacets, addRefineToggleRangeVariations, - addRefineToggleWithAutomaticFacets, addRefineToggleWithDependsOnFacetAndNumerical, addRefineToggleWithStaticFacets, addRefineToggleWithStaticFacetsAndNoManager, @@ -180,130 +177,6 @@ describe('Refine Toggle Test Suites', () => { }); }); - describe('when the modal is opened with automatic facets only', () => { - const collapseFacetsAfter = 2; - beforeEach(() => { - new TestFixture() - .with( - addRefineToggleWithAutomaticFacets({ - 'collapse-facets-after': collapseFacetsAfter, - }) - ) - .withMobileViewport() - .init(); - RefineToggleSelectors.buttonOpen().click(); - }); - - it('should render the modal', () => { - CommonAssertions.assertContainsComponentErrorWithoutIt( - RefineModalSelectors, - false - ); - CommonAssertions.assertContainsComponentErrorWithoutIt( - RefineModalSelectors, - false - ); - CommonAssertions.assertAccessibilityWithoutIt(refineModalComponent); - CommonAssertions.assertWCAG2_5_3(); - }); - - it('should display the filter section', () => { - RefineModalSelectors.filterSection().should('exist'); - }); - - it('should display the automatic facets', () => { - const automaticFacetAmount = 3; - RefineModalSelectors.automaticFacets() - .children() - .should('have.length', automaticFacetAmount); - }); - - it('should respect the collapseFacetsAfter prop', () => { - RefineModalSelectors.facets() - .find(automaticFacetGeneratorComponent) - .children() - .each(($child, index) => { - if (index + 1 > collapseFacetsAfter) { - cy.wrap($child).should('have.attr', 'is-collapsed'); - return; - } - cy.wrap($child).should('not.have.attr', 'is-collapsed'); - }); - }); - }); - - describe('when the modal is opened with both facets type', () => { - const collapseFacetsAfter = 4; - const staticFacetAmount = 1; - const automaticFacetAmount = 1; - beforeEach(() => { - new TestFixture() - .with( - addFacetManagerWithBothTypesOfFacets({ - 'collapse-facets-after': collapseFacetsAfter, - }) - ) - .withMobileViewport() - .init(); - RefineToggleSelectors.buttonOpen().click(); - }); - - it('should render the modal', () => { - CommonAssertions.assertContainsComponentErrorWithoutIt( - RefineModalSelectors, - false - ); - CommonAssertions.assertContainsComponentErrorWithoutIt( - RefineModalSelectors, - false - ); - CommonAssertions.assertAccessibilityWithoutIt(refineModalComponent); - CommonAssertions.assertWCAG2_5_3(); - }); - - it('should display the filter section', () => { - RefineModalSelectors.filterSection().should('exist'); - }); - - it('should display the automatic facets', () => { - RefineModalSelectors.automaticFacets().should('exist'); - }); - - it('should display both facet types', () => { - RefineModalSelectors.automaticFacets() - .children() - .should('have.length', automaticFacetAmount); - - const allStaticFacet = [facetComponent]; - RefineModalSelectors.facets() - .children() - .should('have.length', allStaticFacet.length + 1); - }); - - it('should respect the collapseFacetsAfter prop', () => { - RefineModalSelectors.facets() - .children() - .each(($child, index) => { - if (index + 1 > collapseFacetsAfter) { - cy.wrap($child).should('have.attr', 'is-collapsed'); - return; - } - cy.wrap($child).should('not.have.attr', 'is-collapsed'); - }); - - RefineModalSelectors.facets() - .find(automaticFacetGeneratorComponent) - .children() - .each(($child, index) => { - if (index + 1 > collapseFacetsAfter - staticFacetAmount) { - cy.wrap($child).should('have.attr', 'is-collapsed'); - return; - } - cy.wrap($child).should('not.have.attr', 'is-collapsed'); - }); - }); - }); - describe('when the modal is opened with range facet variations', () => { beforeEach(() => { new TestFixture() diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts index d267ccb8b38..893b4c78f26 100644 --- a/packages/atomic/src/components.d.ts +++ b/packages/atomic/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { AutomaticFacet, CategoryFacetSortCriterion, DateFilterRange, DateRangeRequest, FacetResultsMustMatch, FacetSortCriterion, GeneratedAnswer, GeneratedAnswerCitation, InlineLink, InteractiveCitation, NumericFilter, NumericFilterState, RangeFacetRangeAlgorithm, RangeFacetSortCriterion, RelativeDateUnit, Result, ResultTemplate, ResultTemplateCondition, SearchStatus } from "@coveo/headless"; +import { CategoryFacetSortCriterion, DateFilterRange, DateRangeRequest, FacetResultsMustMatch, FacetSortCriterion, GeneratedAnswer, GeneratedAnswerCitation, InlineLink, InteractiveCitation, NumericFilter, NumericFilterState, RangeFacetRangeAlgorithm, RangeFacetSortCriterion, RelativeDateUnit, Result, ResultTemplate, ResultTemplateCondition } from "@coveo/headless"; import { AnyBindings } from "./components/common/interface/bindings"; import { NumberInputType } from "./components/common/facets/facet-number-input/number-input-type"; import { ItemDisplayBasicLayout, ItemDisplayDensity, ItemDisplayImageSize, ItemDisplayLayout } from "./components/common/layout/display-options"; @@ -19,7 +19,7 @@ import { RecsStore } from "./components/recommendations/atomic-recs-interface/st import { RedirectionPayload } from "./components/common/search-box/redirection-payload"; import { i18n } from "i18next"; import { SearchBoxSuggestionElement } from "./components/common/suggestions/suggestions-types"; -export { AutomaticFacet, CategoryFacetSortCriterion, DateFilterRange, DateRangeRequest, FacetResultsMustMatch, FacetSortCriterion, GeneratedAnswer, GeneratedAnswerCitation, InlineLink, InteractiveCitation, NumericFilter, NumericFilterState, RangeFacetRangeAlgorithm, RangeFacetSortCriterion, RelativeDateUnit, Result, ResultTemplate, ResultTemplateCondition, SearchStatus } from "@coveo/headless"; +export { CategoryFacetSortCriterion, DateFilterRange, DateRangeRequest, FacetResultsMustMatch, FacetSortCriterion, GeneratedAnswer, GeneratedAnswerCitation, InlineLink, InteractiveCitation, NumericFilter, NumericFilterState, RangeFacetRangeAlgorithm, RangeFacetSortCriterion, RelativeDateUnit, Result, ResultTemplate, ResultTemplateCondition } from "@coveo/headless"; export { AnyBindings } from "./components/common/interface/bindings"; export { NumberInputType } from "./components/common/facets/facet-number-input/number-input-type"; export { ItemDisplayBasicLayout, ItemDisplayDensity, ItemDisplayImageSize, ItemDisplayLayout } from "./components/common/layout/display-options"; @@ -34,21 +34,6 @@ export { RedirectionPayload } from "./components/common/search-box/redirection-p export { i18n } from "i18next"; export { SearchBoxSuggestionElement } from "./components/common/suggestions/suggestions-types"; export namespace Components { - /** - * An automatic facet is a special type of facet generated by the automatic facets feature. - * Unlike regular facets, which need to be explicitly defined and requested in the query, - * automatic facets are dynamically generated by the index in response to the query. - * **Note:** This component should never be used on its own. It is used internally by the `atomic-automatic-facet-generator` - * component to automatically render updated facets. However, you can use the shadow parts to style the automatically generated facets. - * To learn more about the automatic facet generator feature, see: [About the Facet Generator](https://docs.coveo.com/en/n9sd0159/). - */ - interface AtomicAutomaticFacet { - "facet": AutomaticFacet; - "facetId": string; - "field": string; - "isCollapsed": boolean; - "searchStatus": SearchStatus; - } /** * The `atomic-automatic-facet-generator` is a component that renders the facets from * the automatic facets feature. Unlike regular facets, which need to be explicitly defined @@ -2142,20 +2127,6 @@ export interface AtomicStencilFacetDateInputCustomEvent extends CustomEvent; /** * The `atomic-automatic-facet-generator` is a component that renders the facets from * the automatic facets feature. Unlike regular facets, which need to be explicitly defined diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.mdx b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.mdx new file mode 100644 index 00000000000..e2cfdc9bf09 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.mdx @@ -0,0 +1,30 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { AtomicDocTemplate } from '../../../../storybook-utils/documentation/atomic-doc-template'; +import * as AtomicAutomaticFacetStories from './atomic-automatic-facet.new.stories'; + + + + + +To use automatic facets, add the `atomic-automatic-facet-generator` component within the "facets" section of the layout. + +```html + + ... + + ... + + + + + +``` + +Learn more about automatic facets: [About the Facet Generator](https://docs.coveo.com/en/n9sd0159/). + + diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.new.stories.tsx b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.new.stories.tsx new file mode 100644 index 00000000000..489490e72db --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.new.stories.tsx @@ -0,0 +1,57 @@ +import type { + Decorator, + Meta, + StoryObj as Story, +} from '@storybook/web-components-vite'; +import {html} from 'lit'; +import {MockSearchApi} from '@/storybook-utils/api/search/mock'; +import {parameters} from '@/storybook-utils/common/common-meta-parameters'; +import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper'; + +const mockSearchApi = new MockSearchApi(); + +const {decorator, play} = wrapInSearchInterface(); + +const facetWidthDecorator: Decorator = (story) => + html`
${story()}
`; + +const meta: Meta = { + component: 'atomic-automatic-facet', + title: 'Search/Automatic Facet', + id: 'atomic-automatic-facet', + render: () => + html``, + decorators: [facetWidthDecorator, decorator], + parameters: { + ...parameters, + msw: {handlers: [...mockSearchApi.handlers]}, + }, + beforeEach: async () => { + mockSearchApi.searchEndpoint.clear(); + }, + play, +}; + +export default meta; + +export const Default: Story = { + beforeEach: async () => { + mockSearchApi.searchEndpoint.mockOnce((response) => ({ + ...response, + generateAutomaticFacets: { + facets: [ + { + field: 'objecttype', + label: 'Type', + values: [ + {value: 'Document', numberOfResults: 45, state: 'idle'}, + {value: 'PDF', numberOfResults: 32, state: 'idle'}, + {value: 'Video', numberOfResults: 18, state: 'idle'}, + {value: 'Image', numberOfResults: 12, state: 'idle'}, + ], + }, + ], + }, + })); + }, +}; diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.spec.ts b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.spec.ts new file mode 100644 index 00000000000..e70ff004a53 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.spec.ts @@ -0,0 +1,248 @@ +import type { + AutomaticFacet, + AutomaticFacetState, + SearchStatus, +} from '@coveo/headless'; +import {html} from 'lit'; +import {describe, expect, it, vi} from 'vitest'; +import {page, userEvent} from 'vitest/browser'; +import {renderInAtomicSearchInterface} from '@/vitest-utils/testing-helpers/fixtures/atomic/search/atomic-search-interface-fixture'; +import {buildFakeAutomaticFacet} from '@/vitest-utils/testing-helpers/fixtures/headless/search/automatic-facet-controller'; +import {buildFakeSearchEngine} from '@/vitest-utils/testing-helpers/fixtures/headless/search/engine'; +import {buildFakeSearchStatus} from '@/vitest-utils/testing-helpers/fixtures/headless/search/search-status-controller'; +import type {AtomicAutomaticFacet} from './atomic-automatic-facet'; +import './atomic-automatic-facet'; + +vi.mock('@coveo/headless', {spy: true}); + +describe('atomic-automatic-facet', () => { + const mockedEngine = buildFakeSearchEngine(); + let mockedFacet: AutomaticFacet; + let mockedSearchStatus: SearchStatus; + const mockedToggleSelect = vi.fn(); + const mockedDeselectAll = vi.fn(); + + interface RenderAutomaticFacetOptions { + field?: string; + facetId?: string; + isCollapsed?: boolean; + state?: Partial; + searchStatusHasError?: boolean; + } + + const renderAutomaticFacet = async ({ + field = 'objecttype', + facetId = 'automatic-facet-1', + isCollapsed = false, + state = {}, + searchStatusHasError = false, + }: RenderAutomaticFacetOptions = {}) => { + mockedFacet = buildFakeAutomaticFacet({ + state: { + field, + label: 'Type', + values: [ + {value: 'Document', numberOfResults: 45, state: 'idle'}, + {value: 'PDF', numberOfResults: 32, state: 'idle'}, + ], + ...state, + }, + implementation: { + toggleSelect: mockedToggleSelect, + deselectAll: mockedDeselectAll, + }, + }); + + mockedSearchStatus = buildFakeSearchStatus({ + state: { + firstSearchExecuted: true, + hasError: searchStatusHasError, + }, + }); + + const {element} = await renderInAtomicSearchInterface( + { + template: html`
+ +
`, + selector: 'atomic-automatic-facet', + bindings: (bindings) => { + bindings.engine = mockedEngine; + bindings.store.getUniqueIDFromEngine = vi.fn().mockReturnValue('123'); + return bindings; + }, + } + ); + + return { + element, + label: () => page.getByRole('button', {name: /Type/i}), + clearButton: () => page.getByLabelText(/Clear filter/i), + value: (name: string) => page.getByText(name), + parts: (element: AtomicAutomaticFacet) => { + const qs = (part: string) => + element.shadowRoot?.querySelector(`[part~="${part}"]`); + return { + facet: qs('facet'), + labelButton: qs('label-button'), + labelButtonIcon: qs('label-button-icon'), + clearButton: qs('clear-button'), + clearButtonIcon: qs('clear-button-icon'), + values: element.shadowRoot?.querySelector('[part="values"]'), + valueCheckboxes: element.shadowRoot?.querySelectorAll( + '[part~="value-checkbox"]' + ), + valueLabels: element.shadowRoot?.querySelectorAll( + '[part~="value-label"]' + ), + valueCounts: element.shadowRoot?.querySelectorAll( + '[part~="value-count"]' + ), + }; + }, + }; + }; + + it('should render the facet with correct label', async () => { + const {label} = await renderAutomaticFacet(); + await expect.element(label()).toBeInTheDocument(); + }); + + it('should use field name when label is undefined', async () => { + await renderAutomaticFacet({ + field: 'my_custom_field', + state: {label: undefined}, + }); + await expect + .element(page.getByRole('button', {name: /my_custom_field/i})) + .toBeInTheDocument(); + }); + + it('should render facet values', async () => { + const {value} = await renderAutomaticFacet(); + await expect.element(value('Document')).toBeInTheDocument(); + await expect.element(value('PDF')).toBeInTheDocument(); + }); + + it('should display the correct number of results for each value', async () => { + await renderAutomaticFacet(); + await expect.element(page.getByText('(45)')).toBeInTheDocument(); + await expect.element(page.getByText('(32)')).toBeInTheDocument(); + }); + + it('should hide values when collapsed', async () => { + const {element, parts} = await renderAutomaticFacet({isCollapsed: true}); + expect(parts(element).values).toBeNull(); + }); + + it('should show values when expanded', async () => { + const {element, parts} = await renderAutomaticFacet({isCollapsed: false}); + expect(parts(element).values).not.toBeNull(); + }); + + it('should toggle collapse when label button is clicked', async () => { + const {element, label} = await renderAutomaticFacet({isCollapsed: false}); + + await userEvent.click(label()!); + expect(element.isCollapsed).toBe(true); + + await userEvent.click(label()!); + expect(element.isCollapsed).toBe(false); + }); + + it('should call toggleSelect when a value is clicked', async () => { + const {element} = await renderAutomaticFacet(); + const firstCheckbox = element.shadowRoot?.querySelector( + '[part~="value-checkbox"]' + ) as HTMLElement; + + await userEvent.click(firstCheckbox); + expect(mockedToggleSelect).toHaveBeenCalledWith( + mockedFacet.state.values[0] + ); + }); + + it('should not show clear button when no values are selected', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).clearButton).toBeNull(); + }); + + it('should call deselectAll when clear button is clicked', async () => { + const {element, parts} = await renderAutomaticFacet({ + state: { + values: [{value: 'Document', numberOfResults: 45, state: 'selected'}], + }, + }); + + const clearButton = parts(element).clearButton as HTMLElement; + await userEvent.click(clearButton); + expect(mockedDeselectAll).toHaveBeenCalled(); + }); + + describe('shadow parts', () => { + it('should expose facet part', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).facet).not.toBeNull(); + }); + + it('should expose label-button part', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).labelButton).not.toBeNull(); + }); + + it('should expose label-button-icon part', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).labelButtonIcon).not.toBeNull(); + }); + + it('should expose values part when expanded', async () => { + const {element, parts} = await renderAutomaticFacet({isCollapsed: false}); + expect(parts(element).values).not.toBeNull(); + }); + + it('should expose value-checkbox parts', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).valueCheckboxes?.length).toBe(2); + }); + + it('should expose value-label parts', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).valueLabels?.length).toBe(2); + }); + + it('should expose value-count parts', async () => { + const {element, parts} = await renderAutomaticFacet(); + expect(parts(element).valueCounts?.length).toBe(2); + }); + + describe('when values are selected', () => { + it('should expose clear-button part', async () => { + const {element, parts} = await renderAutomaticFacet({ + state: { + values: [ + {value: 'Document', numberOfResults: 45, state: 'selected'}, + ], + }, + }); + expect(parts(element).clearButton).not.toBeNull(); + }); + + it('should expose clear-button-icon part', async () => { + const {element, parts} = await renderAutomaticFacet({ + state: { + values: [ + {value: 'Document', numberOfResults: 45, state: 'selected'}, + ], + }, + }); + expect(parts(element).clearButtonIcon).not.toBeNull(); + }); + }); + }); +}); diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.ts b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.ts new file mode 100644 index 00000000000..3314546bc44 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/atomic-automatic-facet.ts @@ -0,0 +1,200 @@ +import {isNullOrUndefined} from '@coveo/bueno'; +import type {AutomaticFacet, FacetValue, SearchStatus} from '@coveo/headless'; +import {html, LitElement, nothing} from 'lit'; +import {customElement, property, state} from 'lit/decorators.js'; +import {when} from 'lit/directives/when.js'; +import facetCommonStyles from '@/src/components/common/facets/facet-common.tw.css'; +import {renderFacetContainer} from '@/src/components/common/facets/facet-container/facet-container'; +import {renderFacetHeader} from '@/src/components/common/facets/facet-header/facet-header'; +import {renderFacetValueCheckbox} from '@/src/components/common/facets/facet-value-checkbox/facet-value-checkbox'; +import facetValueCheckboxStyles from '@/src/components/common/facets/facet-value-checkbox/facet-value-checkbox.tw.css'; +import {renderFacetValueLabelHighlight} from '@/src/components/common/facets/facet-value-label-highlight/facet-value-label-highlight'; +import {renderFacetValuesGroup} from '@/src/components/common/facets/facet-values-group/facet-values-group'; +import type {Bindings} from '@/src/components/search/atomic-search-interface/atomic-search-interface'; +import {bindingGuard} from '@/src/decorators/binding-guard'; +import {bindings} from '@/src/decorators/bindings'; +import {errorGuard} from '@/src/decorators/error-guard'; +import type {InitializableComponent} from '@/src/decorators/types'; +import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles'; +import {InitializeBindingsMixin} from '@/src/mixins/bindings-mixin'; +import {FocusTargetController} from '@/src/utils/accessibility-utils'; +import {getFieldValueCaption} from '@/src/utils/field-utils'; + +/** + * The `atomic-automatic-facet` component is a special type of facet generated by the automatic facets feature. + * Unlike regular facets, which need to be explicitly defined and requested in the query, + * automatic facets are dynamically generated by the index in response to the query. + * + * **Note:** This component should never be used on its own. It is used internally by the `atomic-automatic-facet-generator` + * component to automatically render updated facets. However, you can use the shadow parts to style the automatically generated facets. + * + * To learn more about the automatic facet generator feature, see: [About the Facet Generator](https://docs.coveo.com/en/n9sd0159/). + * + * @part facet - The wrapper for the entire facet. + * + * @part label-button - The button that displays the label and allows to expand/collapse the facet. + * @part label-button-icon - The label button icon. + * @part clear-button - The button that resets the actively selected facet values. + * @part clear-button-icon - The clear button icon. + * + * @part values - The facet values container. + * @part value-label - The facet value label, common for all displays. + * @part value-count - The facet value count, common for all displays. + * + * @part value-checkbox - The facet value checkbox, available when display is 'checkbox'. + * @part value-checkbox-checked - The checked facet value checkbox, available when display is 'checkbox'. + * @part value-checkbox-label - The facet value checkbox clickable label, available when display is 'checkbox'. + * @part value-checkbox-icon - The facet value checkbox icon, available when display is 'checkbox'. + */ +@customElement('atomic-automatic-facet') +@bindings() +@withTailwindStyles +export class AtomicAutomaticFacet + extends InitializeBindingsMixin(LitElement) + implements InitializableComponent +{ + static styles = [facetCommonStyles, facetValueCheckboxStyles]; + + @state() bindings!: Bindings; + @state() public error!: Error; + + /** + * The field associated with the automatic facet. + * @internal + */ + @property({type: String, reflect: true}) public field!: string; + + /** + * The unique identifier for the automatic facet. + * @internal + */ + @property({type: String, attribute: 'facet-id', reflect: true}) + public facetId!: string; + + /** + * The automatic facet controller instance. + * @internal + */ + @property({type: Object, reflect: true}) public facet!: AutomaticFacet; + + /** + * The search status controller instance. + * @internal + */ + @property({type: Object, attribute: 'search-status', reflect: true}) + public searchStatus!: SearchStatus; + + /** + * Whether the facet is collapsed. + * @internal + */ + @property({type: Boolean, attribute: 'is-collapsed', reflect: true}) + public isCollapsed = false; + + private headerFocus?: FocusTargetController; + + public initialize() {} + + private get focusTarget() { + if (!this.headerFocus) { + this.headerFocus = new FocusTargetController(this, this.bindings); + } + return this.headerFocus; + } + + private get numberOfSelectedValues() { + return this.facet.state.values.filter((value) => this.isSelected(value)) + .length; + } + + private isSelected(value: FacetValue) { + return value.state === 'selected'; + } + + private get label() { + return isNullOrUndefined(this.facet.state.label) + ? this.facet.state.field + : this.facet.state.label; + } + + private renderValue(facetValue: FacetValue, onClick: () => void) { + const displayValue = getFieldValueCaption( + this.facet.state.field, + facetValue.value, + this.bindings.i18n + ); + + return renderFacetValueCheckbox({ + props: { + displayValue, + numberOfResults: facetValue.numberOfResults, + isSelected: this.isSelected(facetValue), + i18n: this.bindings.i18n, + onClick, + }, + })( + html`${renderFacetValueLabelHighlight({ + props: { + displayValue, + isSelected: this.isSelected(facetValue), + }, + })}` + ); + } + + private renderValuesContainer(children: unknown[], query?: string) { + return renderFacetValuesGroup({ + props: { + i18n: this.bindings.i18n, + label: this.facet.state.label, + query, + }, + })(html`
    ${children}
`); + } + + private renderValues() { + return this.renderValuesContainer( + this.facet.state.values.map((value) => + this.renderValue(value, () => this.facet.toggleSelect(value)) + ) + ); + } + + private renderHeader() { + return renderFacetHeader({ + props: { + i18n: this.bindings.i18n, + label: this.label, + onClearFilters: () => { + this.focusTarget.focusAfterSearch(); + this.facet.deselectAll(); + }, + numberOfActiveValues: this.numberOfSelectedValues, + isCollapsed: this.isCollapsed, + headingLevel: 0, + onToggleCollapse: () => { + this.isCollapsed = !this.isCollapsed; + }, + headerRef: (el) => this.focusTarget.setTarget(el), + }, + }); + } + + @bindingGuard() + @errorGuard() + render() { + if (this.searchStatus.state.hasError) { + return html`${nothing}`; + } + + return renderFacetContainer()(html` + ${this.renderHeader()} ${when(!this.isCollapsed, () => this.renderValues())} + `); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'atomic-automatic-facet': AtomicAutomaticFacet; + } +} diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/e2e/atomic-automatic-facet.e2e.ts b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/atomic-automatic-facet.e2e.ts new file mode 100644 index 00000000000..562d3136fde --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/atomic-automatic-facet.e2e.ts @@ -0,0 +1,19 @@ +import {expect, test} from './fixture'; + +test.describe('atomic-automatic-facet', () => { + test.beforeEach(async ({automaticFacet}) => { + await automaticFacet.load({story: 'default'}); + await automaticFacet.hydrated.waitFor(); + }); + + test('should render the automatic facet with label', async ({ + automaticFacet, + }) => { + await expect(automaticFacet.facetLabel).toBeVisible(); + await expect(automaticFacet.facetLabel).toContainText('Type'); + }); + + test('should display facet values', async ({automaticFacet}) => { + await expect(automaticFacet.facetValues).toHaveCount(4); + }); +}); diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/e2e/fixture.ts b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/fixture.ts new file mode 100644 index 00000000000..4b316e8a0a2 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/fixture.ts @@ -0,0 +1,14 @@ +import {test as base} from '@playwright/test'; +import {AtomicAutomaticFacetPageObject as AutomaticFacet} from './page-object'; + +type MyFixture = { + automaticFacet: AutomaticFacet; +}; + +export const test = base.extend({ + automaticFacet: async ({page}, use) => { + await use(new AutomaticFacet(page)); + }, +}); + +export {expect} from '@playwright/test'; diff --git a/packages/atomic/src/components/search/atomic-automatic-facet/e2e/page-object.ts b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/page-object.ts new file mode 100644 index 00000000000..e8c8378d155 --- /dev/null +++ b/packages/atomic/src/components/search/atomic-automatic-facet/e2e/page-object.ts @@ -0,0 +1,24 @@ +import type {Page} from '@playwright/test'; +import {BasePageObject} from '@/playwright-utils/lit-base-page-object'; + +export class AtomicAutomaticFacetPageObject extends BasePageObject { + constructor(page: Page) { + super(page, 'atomic-automatic-facet'); + } + + get facetLabel() { + return this.page.locator('atomic-automatic-facet [part="label-button"]'); + } + + get facetValues() { + return this.page.locator('atomic-automatic-facet [part="values"] > li'); + } + + get clearButton() { + return this.page.locator('atomic-automatic-facet [part="clear-button"]'); + } + + get firstFacetValue() { + return this.facetValues.first(); + } +} diff --git a/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.pcss b/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.pcss deleted file mode 100644 index adb8e6746b4..00000000000 --- a/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.pcss +++ /dev/null @@ -1,3 +0,0 @@ -@import '../../../../global/global.pcss'; -@import '../../../common/facets/facet-common.pcss'; -@import '../../../common/facets/facet-value-checkbox/facet-value-checkbox.pcss'; diff --git a/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.tsx b/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.tsx deleted file mode 100644 index 4673baceec9..00000000000 --- a/packages/atomic/src/components/search/facets/atomic-automatic-facet/atomic-automatic-facet.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import {isNullOrUndefined} from '@coveo/bueno'; -import {AutomaticFacet, SearchStatus, FacetValue} from '@coveo/headless'; -import {Component, Prop, State, h, VNode} from '@stencil/core'; -import {getFieldValueCaption} from '../../../../utils/field-utils'; -import { - InitializableComponent, - InitializeBindings, -} from '../../../../utils/initialization-utils'; -import {FocusTargetController} from '../../../../utils/stencil-accessibility-utils'; -import {FacetContainer} from '../../../common/facets/facet-container/stencil-facet-container'; -import {FacetHeader} from '../../../common/facets/facet-header/stencil-facet-header'; -import {FacetValueCheckbox} from '../../../common/facets/facet-value-checkbox/stencil-facet-value-checkbox'; -import {FacetValueLabelHighlight} from '../../../common/facets/facet-value-label-highlight/stencil-facet-value-label-highlight'; -import {FacetValuesGroup} from '../../../common/facets/facet-values-group/stencil-facet-values-group'; -import {Hidden} from '../../../common/stencil-hidden'; -import {Bindings} from '../../atomic-search-interface/atomic-search-interface'; - -/** - * An automatic facet is a special type of facet generated by the automatic facets feature. - * Unlike regular facets, which need to be explicitly defined and requested in the query, - * automatic facets are dynamically generated by the index in response to the query. - * - * **Note:** This component should never be used on its own. It is used internally by the `atomic-automatic-facet-generator` - * component to automatically render updated facets. However, you can use the shadow parts to style the automatically generated facets. - * - * To learn more about the automatic facet generator feature, see: [About the Facet Generator](https://docs.coveo.com/en/n9sd0159/). - * - * @part facet - The wrapper for the entire facet. - * - * @part label-button - The button that displays the label and allows to expand/collapse the facet. - * @part label-button-icon - The label button icon. - * @part clear-button - The button that resets the actively selected facet values. - * @part clear-button-icon - The clear button icon. - * - * @part values - The facet values container. - * @part value-label - The facet value label, common for all displays. - * @part value-count - The facet value count, common for all displays. - * - * @part value-checkbox - The facet value checkbox, available when display is 'checkbox'. - * @part value-checkbox-checked - The checked facet value checkbox, available when display is 'checkbox'. - * @part value-checkbox-label - The facet value checkbox clickable label, available when display is 'checkbox'. - * @part value-checkbox-icon - The facet value checkbox icon, available when display is 'checkbox'. - */ -@Component({ - tag: 'atomic-automatic-facet', - styleUrl: 'atomic-automatic-facet.pcss', - shadow: true, -}) -export class AtomicAutomaticFacet implements InitializableComponent { - @InitializeBindings() public bindings!: Bindings; - @State() public error!: Error; - - /** - * @internal - */ - @Prop({reflect: true}) public field!: string; - /** - * @internal - */ - @Prop({reflect: true}) public facetId!: string; - /** - * @internal - */ - @Prop({reflect: true}) public facet!: AutomaticFacet; - /** - * @internal - */ - @Prop({reflect: true}) public searchStatus!: SearchStatus; - /** - * @internal - */ - @Prop({reflect: true, mutable: true}) public isCollapsed!: boolean; - - private headerFocus?: FocusTargetController; - - private get focusTarget() { - if (!this.headerFocus) { - this.headerFocus = new FocusTargetController(this); - } - return this.headerFocus; - } - - private get numberOfSelectedValues() { - return this.facet.state.values.filter((value) => this.isSelected(value)) - .length; - } - - private isSelected(value: FacetValue) { - return value.state === 'selected'; - } - - private renderValue(facetValue: FacetValue, onClick: () => void) { - const displayValue = getFieldValueCaption( - this.facet.state.field, - facetValue.value, - this.bindings.i18n - ); - - return ( - - - - ); - } - - private renderValuesContainer(children: VNode[], query?: string) { - return ( - -
    - {children} -
-
- ); - } - - private renderValues() { - return this.renderValuesContainer( - this.facet.state.values.map((value) => - this.renderValue(value, () => this.facet.toggleSelect(value)) - ) - ); - } - - private get label() { - return isNullOrUndefined(this.facet.state.label) - ? this.facet.state.field - : this.facet.state.label; - } - - public renderHeader() { - return ( - { - this.focusTarget.focusAfterSearch(); - this.facet.deselectAll(); - }} - numberOfActiveValues={this.numberOfSelectedValues} - isCollapsed={this.isCollapsed} - headingLevel={0} - onToggleCollapse={() => (this.isCollapsed = !this.isCollapsed)} - headerRef={(el) => this.focusTarget.setTarget(el)} - > - ); - } - - public render() { - if (this.searchStatus.state.hasError) { - return ; - } - - return ( - - {this.renderHeader()} - {!this.isCollapsed && this.renderValues()} - - ); - } -} diff --git a/packages/atomic/src/components/search/index.ts b/packages/atomic/src/components/search/index.ts index 74563ec18a3..3b14a8e846a 100644 --- a/packages/atomic/src/components/search/index.ts +++ b/packages/atomic/src/components/search/index.ts @@ -1,4 +1,5 @@ // Auto-generated file +export {AtomicAutomaticFacet} from './atomic-automatic-facet/atomic-automatic-facet.js'; export {AtomicExternal} from './atomic-external/atomic-external.js'; export {AtomicFacet} from './atomic-facet/atomic-facet.js'; export {AtomicFieldCondition} from './atomic-field-condition/atomic-field-condition.js'; diff --git a/packages/atomic/src/components/search/lazy-index.ts b/packages/atomic/src/components/search/lazy-index.ts index a9457594d42..ebe57a9f1a0 100644 --- a/packages/atomic/src/components/search/lazy-index.ts +++ b/packages/atomic/src/components/search/lazy-index.ts @@ -1,5 +1,7 @@ // Auto-generated file export default { + 'atomic-automatic-facet': async () => + await import('./atomic-automatic-facet/atomic-automatic-facet.js'), 'atomic-external': async () => await import('./atomic-external/atomic-external.js'), 'atomic-facet': async () => await import('./atomic-facet/atomic-facet.js'), diff --git a/packages/atomic/src/utils/custom-element-tags.ts b/packages/atomic/src/utils/custom-element-tags.ts index fc9bcea2054..fea8aecbbab 100644 --- a/packages/atomic/src/utils/custom-element-tags.ts +++ b/packages/atomic/src/utils/custom-element-tags.ts @@ -8,6 +8,7 @@ */ export const ATOMIC_CUSTOM_ELEMENT_TAGS = new Set([ 'atomic-aria-live', + 'atomic-automatic-facet', 'atomic-commerce-breadbox', 'atomic-commerce-category-facet', 'atomic-commerce-did-you-mean', diff --git a/packages/atomic/vitest-utils/testing-helpers/fixtures/headless/search/automatic-facet-controller.ts b/packages/atomic/vitest-utils/testing-helpers/fixtures/headless/search/automatic-facet-controller.ts new file mode 100644 index 00000000000..bcd095737d7 --- /dev/null +++ b/packages/atomic/vitest-utils/testing-helpers/fixtures/headless/search/automatic-facet-controller.ts @@ -0,0 +1,43 @@ +import type {AutomaticFacet, AutomaticFacetState} from '@coveo/headless'; +import {vi} from 'vitest'; + +export const defaultState: AutomaticFacetState = { + field: 'test_field', + label: 'Test Field', + values: [ + { + value: 'value-1', + numberOfResults: 15, + state: 'idle', + }, + { + value: 'value-2', + numberOfResults: 8, + state: 'idle', + }, + ], +}; + +export const defaultImplementation = { + subscribe: (subscribedFunction: () => void) => { + subscribedFunction(); + }, + state: defaultState, + toggleSelect: vi.fn(), + toggleSingleSelect: vi.fn(), + isValueSelected: vi.fn(), + deselectAll: vi.fn(), +}; + +export const buildFakeAutomaticFacet = ({ + implementation, + state, +}: Partial<{ + implementation?: Partial; + state?: Partial; +}> = {}): AutomaticFacet => + ({ + ...defaultImplementation, + ...implementation, + ...{state: {...defaultState, ...(state || {})}}, + }) as AutomaticFacet;