Skip to content

Commit

Permalink
test(atomic): add atomic-commerce-facet tests (#4123)
Browse files Browse the repository at this point in the history
Add tests for `atomic-commerce-facet`.

You will see some shenanigan in story setup to "keep only a single
facet".

As you know, atomic-commerce-facet are generated/internal: So we need to
trick storybook a bit to output `atomic-commerce-facets` (with an `s`),
and then "snipe" components that are not of the correct type (ie:
everything that is not an `atomic-commerce-facet`).

This works pretty well for the test themselves. We get a page with only
a specific type of facet, and we can run test as normal.

There will be work needed still to make storybook work correctly with
the Shadow parts.

The shadow parts are properly documented; It's just that when they get
customized in Storybook UI, the styling output is not reflected
correctly in the story.

However, I prefer to continue with testing and revisit that at a later
time.

https://coveord.atlassian.net/browse/KIT-3257

---------

Co-authored-by: GitHub Actions Bot <>
  • Loading branch information
olamothe authored Jul 2, 2024
1 parent ce6fb7e commit 05bfaff
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ AtomicAutomaticFacetGenerator,
AtomicBreadbox,
AtomicCategoryFacet,
AtomicColorFacet,
AtomicCommerceFacet,
AtomicCommerceFacets,
AtomicCommerceLoadMoreProducts,
AtomicCommerceQuerySummary,
Expand Down Expand Up @@ -112,6 +113,7 @@ AtomicAutomaticFacetGenerator,
AtomicBreadbox,
AtomicCategoryFacet,
AtomicColorFacet,
AtomicCommerceFacet,
AtomicCommerceFacets,
AtomicCommerceLoadMoreProducts,
AtomicCommerceQuerySummary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,27 @@ export class AtomicColorFacet {
export declare interface AtomicColorFacet extends Components.AtomicColorFacet {}


@ProxyCmp({
})
@Component({
selector: 'atomic-commerce-facet',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: [],
})
export class AtomicCommerceFacet {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}


export declare interface AtomicCommerceFacet extends Components.AtomicCommerceFacet {}


@ProxyCmp({
inputs: ['collapseFacetsAfter']
})
Expand Down
6 changes: 6 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,12 @@ export namespace Components {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
* @alpha
*/
interface AtomicCommerceFacet {
/**
* The facet controller instance.
* @
*/
"facet": RegularFacet;
/**
Expand Down Expand Up @@ -3495,6 +3497,7 @@ declare global {
};
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
* @alpha
*/
interface HTMLAtomicCommerceFacetElement extends Components.AtomicCommerceFacet, HTMLStencilElement {
}
Expand Down Expand Up @@ -5726,10 +5729,12 @@ declare namespace LocalJSX {
}
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
* @alpha
*/
interface AtomicCommerceFacet {
/**
* The facet controller instance.
* @
*/
"facet": RegularFacet;
/**
Expand Down Expand Up @@ -8830,6 +8835,7 @@ declare module "@stencil/core" {
"atomic-commerce-did-you-mean": LocalJSX.AtomicCommerceDidYouMean & JSXBase.HTMLAttributes<HTMLAtomicCommerceDidYouMeanElement>;
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
* @alpha
*/
"atomic-commerce-facet": LocalJSX.AtomicCommerceFacet & JSXBase.HTMLAttributes<HTMLAtomicCommerceFacetElement>;
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
wrapInCommerceInterface,
playExecuteFirstSearch,
playKeepOnlyFirstFacetOfType,
} from '@coveo/atomic/storybookUtils/commerce-interface-wrapper';
import {parameters} from '@coveo/atomic/storybookUtils/common-meta-parameters';
import {renderComponent} from '@coveo/atomic/storybookUtils/render-component';
import type {Meta, StoryObj as Story} from '@storybook/web-components';
import {html} from 'lit-html';

const {play, decorator} = wrapInCommerceInterface({skipFirstSearch: true});

const meta: Meta = {
component: 'atomic-commerce-facet',
title: 'Atomic-Commerce/Facet',
id: 'atomic-commerce-facet',
render: renderComponent,
decorators: [decorator],
parameters,
play,
};

export default meta;

export const Default: Story = {
name: 'atomic-commerce-facet',
decorators: [
(_) => {
return html`<div id="code-root">
<atomic-commerce-facets></atomic-commerce-facets>
</div>`;
},
],
play: async (context) => {
await play(context);
await playExecuteFirstSearch(context);
playKeepOnlyFirstFacetOfType('atomic-commerce-facet', context);
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,42 @@ import {CommerceBindings as Bindings} from '../../atomic-commerce-interface/atom
/**
* The `atomic-commerce-facet` component renders a commerce facet that the end user can interact with to filter products.
*
* @internal
* @part facet - The wrapper for the entire facet.
* @part placeholder - The placeholder shown before the first search is executed.
*
* @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 search-wrapper - The search box wrapper.
* @part search-input - The search box input.
* @part search-icon - The search box submit button.
* @part search-clear-button - The button to clear the search box of input.
* @part more-matches - The label indicating there are more matches for the current facet search query.
* @part no-matches - The label indicating there are no matches for the current facet search query.
* @part matches-query - The highlighted query inside the matches labels.
* @part search-highlight - The highlighted query inside the facet values.
*
* @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'.
* @part value-link - The facet value when display is 'link'.
* @part value-link-selected - The selected facet value when display is 'link'.
* @part value-box - The facet value when display is 'box'.
* @part value-box-selected - The selected facet value when display is 'box'.
* @part value-exclude-button - The button to exclude a facet value, available when display is 'checkbox'.
*
* @part show-more - The show more results button.
* @part show-less - The show less results button.
* @part show-more-less-icon - The icons of the show more & show less buttons.
*
* @alpha
*/
@Component({
tag: 'atomic-commerce-facet',
Expand All @@ -61,18 +96,26 @@ export class AtomicCommerceFacet implements InitializableComponent<Bindings> {

/**
* The Summary controller instance.
*
* @internal
*/
@Prop() summary!: Summary<SearchSummaryState | ProductListingSummaryState>;
/**
* The facet controller instance.
*
* @@internal
*/
@Prop() public facet!: RegularFacet;
/**
* Specifies whether the facet is collapsed.
*
* @internal
*/
@Prop({reflect: true, mutable: true}) public isCollapsed = false;
/**
* The field identifier for this facet.
*
* @internal
*/
@Prop({reflect: true}) field?: string;

Expand All @@ -90,6 +133,9 @@ export class AtomicCommerceFacet implements InitializableComponent<Bindings> {
protected facetSearchAriaMessage!: string;

public initialize() {
if (!this.facet) {
return;
}
this.initAriaLive();
this.initPopover();
this.registerFacet();
Expand All @@ -113,6 +159,9 @@ export class AtomicCommerceFacet implements InitializableComponent<Bindings> {
}

public render() {
if (!this.facet) {
return;
}
const {hasError, firstRequestExecuted} = this.summary.state;
return (
<FacetGuard
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {test, expect} from './fixture';

test.describe('default', () => {
test.beforeEach(async ({facet}) => {
await facet.load();
});

test('should be A11y compliant', async ({facet, makeAxeBuilder}) => {
await facet.hydrated.waitFor();
const accessibilityResults = await makeAxeBuilder().analyze();
expect(accessibilityResults.violations).toEqual([]);
});

test('should allow to filter by selecting and deselecting a value', async ({
facet,
}) => {
const facetValueLabel = facet.getFacetValue('Nike');
const facetValueBtn = facet.getFacetValueButton('Nike');

await expect(facetValueBtn).not.toBeChecked();
await facetValueLabel.click();

await expect(facetValueBtn).toBeChecked();

await facetValueLabel.click();
await expect(facetValueBtn).not.toBeChecked();
});

test('should allow to filter by selecting multiple values', async ({
facet,
page,
}) => {
const firstValueBtn = facet.getFacetValueButton('Nike');
const secondValueBtn = facet.getFacetValueButton('Adidas');

await expect(firstValueBtn).not.toBeChecked();
await expect(secondValueBtn).not.toBeChecked();
await firstValueBtn.click();
await secondValueBtn.click();

await expect(firstValueBtn).toBeChecked();
await expect(secondValueBtn).toBeChecked();
await expect(page.getByText('Clear 2 filters')).toBeVisible();
});

test('should allow to deselect a filter with the clear button', async ({
facet,
}) => {
const facetValueLabel = facet.getFacetValue('Nike');

await expect(facet.clearFilter).toHaveCount(0);

await facetValueLabel.click();

await expect(facet.clearFilter).toBeVisible();
await facet.clearFilter.click();

await expect(facet.clearFilter).toHaveCount(0);
});

test('should allow to show more values and show less values', async ({
facet,
page,
}) => {
await expect(facet.showMore).toBeVisible();
await expect(facet.showLess).not.toBeVisible();

await expect(page.getByRole('listitem')).toHaveCount(8);

await facet.showMore.click();

await expect(facet.showLess).toBeVisible();
await expect(page.getByRole('listitem')).toHaveCount(16);
await facet.showLess.click();
await expect(page.getByRole('listitem')).toHaveCount(8);
await expect(facet.showLess).not.toBeVisible();
});

test('allow to search for a value', async ({facet, page}) => {
await facet.searchInput.fill('n');

expect(await page.getByRole('listitem').count()).toBeGreaterThanOrEqual(8);
await expect(page.getByText('More matches for n')).toBeVisible();
await facet.searchInput.fill('nike');

await facet.getFacetValue('Nike').click();

await expect(facet.getFacetValueButton('Nike')).toBeChecked();
});

test('allow to clear the search input', async ({facet}) => {
await facet.searchInput.fill('nike');
await expect(facet.clearSearchInput).toBeVisible();

await facet.clearSearchInput.click();
await expect(facet.clearSearchInput).not.toBeVisible();
await expect(facet.searchInput).toBeEmpty();
});

test('behave correct when searching for a value that does not exist', async ({
facet,
page,
}) => {
await facet.searchInput.fill('non-existing-value');

await expect(
page.getByText('No matches found for non-existing-value')
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {test as base} from '@playwright/test';
import {
AxeFixture,
makeAxeBuilder,
} from '../../../../../../playwright-utils/base-fixture';
import {FacetPageObject} from './page-object';

type MyFixtures = {
facet: FacetPageObject;
};

export const test = base.extend<MyFixtures & AxeFixture>({
makeAxeBuilder,
facet: async ({page}, use) => {
await use(new FacetPageObject(page));
},
});

export {expect} from '@playwright/test';
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {Page} from '@playwright/test';
import {BasePageObject} from '../../../../../../playwright-utils/base-page-object';

export class FacetPageObject extends BasePageObject<'atomic-commerce-facet'> {
constructor(page: Page) {
super(page, 'atomic-commerce-facet');
}

get title() {
return this.page.getByText('Brand');
}

get searchInput() {
return this.page.getByPlaceholder('Search');
}

get clearSearchInput() {
return this.page.getByRole('button', {name: 'Clear'});
}

getFacetValue(value: string) {
return this.page.getByRole('listitem').filter({hasText: value});
}

getFacetValueButton(value: string) {
return this.page.getByLabel(`Inclusion filter on ${value}`);
}

get clearFilter() {
return this.page.getByRole('button').filter({hasText: /Clear.*filter/});
}

get showMore() {
return this.page.getByLabel('Show more values');
}

get showLess() {
return this.page.getByLabel('Show less values');
}
}
Loading

0 comments on commit 05bfaff

Please sign in to comment.