Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/atomic-react/src/components/search/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
AtomicResultChildrenTemplate as LitAtomicResultChildrenTemplate,
AtomicResultDate as LitAtomicResultDate,
AtomicResultHtml as LitAtomicResultHtml,
AtomicResultIcon as LitAtomicResultIcon,
AtomicResultImage as LitAtomicResultImage,
AtomicResultList as LitAtomicResultList,
AtomicResultLocalizedText as LitAtomicResultLocalizedText,
Expand Down Expand Up @@ -198,6 +199,12 @@ export const AtomicResultHtml = createComponent({
elementClass: LitAtomicResultHtml,
});

export const AtomicResultIcon = createComponent({
tagName: 'atomic-result-icon',
react: React,
elementClass: LitAtomicResultIcon,
});

export const AtomicResultImage = createComponent({
tagName: 'atomic-result-image',
react: React,
Expand Down
2 changes: 1 addition & 1 deletion packages/atomic/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ dist-storybook/
/dev/headless/

# Generated Tailwind CSS files
*.tw.css.ts
*.tw.css.ts
29 changes: 0 additions & 29 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,12 +1496,6 @@ export namespace Components {
*/
interface AtomicResultFieldsList {
}
/**
* The `atomic-result-icon` component outputs the corresponding icon for a given file type.
* The component searches for a suitable icon, or outputs a generic icon if the search is unsuccessful.
*/
interface AtomicResultIcon {
}
/**
* The `atomic-result-link` component automatically transforms a search result title into a clickable link that points to the original item.
*/
Expand Down Expand Up @@ -2653,16 +2647,6 @@ declare global {
prototype: HTMLAtomicResultFieldsListElement;
new (): HTMLAtomicResultFieldsListElement;
};
/**
* The `atomic-result-icon` component outputs the corresponding icon for a given file type.
* The component searches for a suitable icon, or outputs a generic icon if the search is unsuccessful.
*/
interface HTMLAtomicResultIconElement extends Components.AtomicResultIcon, HTMLStencilElement {
}
var HTMLAtomicResultIconElement: {
prototype: HTMLAtomicResultIconElement;
new (): HTMLAtomicResultIconElement;
};
/**
* The `atomic-result-link` component automatically transforms a search result title into a clickable link that points to the original item.
*/
Expand Down Expand Up @@ -3031,7 +3015,6 @@ declare global {
"atomic-refine-toggle": HTMLAtomicRefineToggleElement;
"atomic-result-children": HTMLAtomicResultChildrenElement;
"atomic-result-fields-list": HTMLAtomicResultFieldsListElement;
"atomic-result-icon": HTMLAtomicResultIconElement;
"atomic-result-link": HTMLAtomicResultLinkElement;
"atomic-result-placeholder": HTMLAtomicResultPlaceholderElement;
"atomic-result-printable-uri": HTMLAtomicResultPrintableUriElement;
Expand Down Expand Up @@ -4471,12 +4454,6 @@ declare namespace LocalJSX {
*/
interface AtomicResultFieldsList {
}
/**
* The `atomic-result-icon` component outputs the corresponding icon for a given file type.
* The component searches for a suitable icon, or outputs a generic icon if the search is unsuccessful.
*/
interface AtomicResultIcon {
}
/**
* The `atomic-result-link` component automatically transforms a search result title into a clickable link that points to the original item.
*/
Expand Down Expand Up @@ -4967,7 +4944,6 @@ declare namespace LocalJSX {
"atomic-refine-toggle": AtomicRefineToggle;
"atomic-result-children": AtomicResultChildren;
"atomic-result-fields-list": AtomicResultFieldsList;
"atomic-result-icon": AtomicResultIcon;
"atomic-result-link": AtomicResultLink;
"atomic-result-placeholder": AtomicResultPlaceholder;
"atomic-result-printable-uri": AtomicResultPrintableUri;
Expand Down Expand Up @@ -5204,11 +5180,6 @@ declare module "@stencil/core" {
* The `atomic-result-fields-list` component selectively renders its children to ensure they fit the parent element and adds dividers between them.
*/
"atomic-result-fields-list": LocalJSX.AtomicResultFieldsList & JSXBase.HTMLAttributes<HTMLAtomicResultFieldsListElement>;
/**
* The `atomic-result-icon` component outputs the corresponding icon for a given file type.
* The component searches for a suitable icon, or outputs a generic icon if the search is unsuccessful.
*/
"atomic-result-icon": LocalJSX.AtomicResultIcon & JSXBase.HTMLAttributes<HTMLAtomicResultIconElement>;
/**
* The `atomic-result-link` component automatically transforms a search result title into a clickable link that points to the original item.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,58 @@
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
import {html} from 'lit';
import {MockSearchApi} from '@/storybook-utils/api/search/mock';
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
import {wrapInResultList} from '@/storybook-utils/search/result-list-wrapper';
import {wrapInResultTemplate} from '@/storybook-utils/search/result-template-wrapper';
import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper';

const {events, args, argTypes, template} = getStorybookHelpers(
'atomic-result-icon',
{excludeCategories: ['methods']}
);
const searchApiHarness = new MockSearchApi();

searchApiHarness.searchEndpoint.mock((response) => ({
name: 'atomic-result-icon',
...response,
results: response.results.slice(0, 1),
totalCount: 1,
totalCountFiltered: 1,
}));

const {decorator: searchInterfaceDecorator, play} = wrapInSearchInterface({
config: {
preprocessRequest: (request) => {
const parsed = JSON.parse(request.body as string);
console.log(parsed);
parsed.numberOfResults = 1;
request.body = JSON.stringify(parsed);
return request;
},
},
includeCodeRoot: false,
});
const {decorator: resultListDecorator} = wrapInResultList('list', false);
const {decorator: resultTemplateDecorator} = wrapInResultTemplate();

const {decorator: resultListDecorator} = wrapInResultList('list');
const {decorator: resultTemplateDecorator} = wrapInResultTemplate(false);
const {events, args, argTypes, template} = getStorybookHelpers(
'atomic-result-icon',
{excludeCategories: ['methods']}
);

const meta: Meta = {
component: 'atomic-result-icon',
title: 'Search/ResultList/ResultIcon',
title: 'Search/Result Icon',
id: 'atomic-result-icon',

render: (args) => template(args),
decorators: [
(story) => html`
<atomic-result-section-visual id="code-root">
${story()}
</atomic-result-section-visual>
`,
resultTemplateDecorator,
resultListDecorator,
searchInterfaceDecorator,
],
parameters: {
...parameters,
msw: {
handlers: [...searchApiHarness.handlers],
},
actions: {
handles: events,
},
},
args,
argTypes,

play,
};

export default meta;

export const Default: Story = {
name: 'atomic-result-icon',
};
export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import type {Result} from '@coveo/headless';
import {html} from 'lit';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {page} from 'vitest/browser';
import {renderInAtomicResult} from '@/vitest-utils/testing-helpers/fixtures/atomic/search/atomic-result-fixture';
import {buildFakeResult} from '@/vitest-utils/testing-helpers/fixtures/headless/search/result';
import {AtomicResultIcon} from './atomic-result-icon';
import './atomic-result-icon';

vi.mock('@coveo/headless', {spy: true});

describe('atomic-result-icon', () => {
let mockResult: Result;

const renderComponent = async (options: {result?: Result} = {}) => {
const resultToUse = options.result ?? mockResult;
const {element, atomicResult, atomicInterface} =
await renderInAtomicResult<AtomicResultIcon>({
template: html`<atomic-result-icon></atomic-result-icon>`,
selector: 'atomic-result-icon',
result: resultToUse,
bindings: (bindings) => {
bindings.engine.logger = {warn: vi.fn()} as never;
bindings.store = {
...bindings.store,
onChange: vi.fn(),
state: {
...bindings.store?.state,
loadingFlags: [],
iconAssetsPath: '',
},
};
return bindings;
},
});

await atomicInterface.updateComplete;
await atomicResult.updateComplete;
await element?.updateComplete;

return {
element,
getAtomicIcon: () => element?.shadowRoot?.querySelector('atomic-icon'),
getSlot: () => element?.shadowRoot?.querySelector('slot'),
};
};

beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: 'pdf',
objecttype: undefined,
urihash: '',
},
});
});

it('should be defined', () => {
const el = document.createElement('atomic-result-icon');
expect(el).toBeInstanceOf(AtomicResultIcon);
});

describe('when the rendered result has a known filetype', () => {
beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: 'pdf',
objecttype: undefined,
urihash: '',
},
});
});

it('should render the icon of the filetype', async () => {
const {getAtomicIcon} = await renderComponent();
const icon = getAtomicIcon();

expect(icon).toBeDefined();
expect(icon?.getAttribute('icon')).toBe('assets://pdf');
expect(icon?.getAttribute('title')).toBe('pdf');
});

it('should not render a slot', async () => {
const {getSlot} = await renderComponent();
expect(getSlot()).toBeNull();
});

it('should be accessible', async () => {
await renderComponent();
await expect.element(page.getByTitle('pdf')).toBeInTheDocument();
});
});

describe('when the rendered result has a known objecttype', () => {
beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: undefined,
objecttype: 'account',
urihash: '',
},
});
});

it('should render the icon of the objecttype', async () => {
const {getAtomicIcon} = await renderComponent();
const icon = getAtomicIcon();

expect(icon).toBeDefined();
expect(icon?.getAttribute('icon')).toBe('assets://account');
expect(icon?.getAttribute('title')).toBe('account');
});

it('should not render a slot', async () => {
const {getSlot} = await renderComponent();
expect(getSlot()).toBeNull();
});
});

describe('when the rendered result has both known filetype and objecttype', () => {
beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: 'pdf',
objecttype: 'account',
urihash: '',
},
});
});

it('should render the objecttype icon (objecttype takes precedence)', async () => {
const {getAtomicIcon} = await renderComponent();
const icon = getAtomicIcon();

expect(icon).toBeDefined();
expect(icon?.getAttribute('icon')).toBe('assets://account');
});
});

describe('when the rendered result has no known filetype or objecttype', () => {
beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: 'unknowntype',
objecttype: 'anotherunknown',
urihash: '',
},
});
});

it('should render a slot with a generic document icon', async () => {
const {getSlot, getAtomicIcon} = await renderComponent();

// Should have a slot for fallback content
expect(getSlot()).toBeDefined();

// The icon should still render with the default 'document' icon
const icon = getAtomicIcon();
expect(icon).toBeDefined();
expect(icon?.getAttribute('icon')).toBe('assets://document');
expect(icon?.getAttribute('title')).toBe('document');
});
});

describe('when the rendered result has undefined filetype and objecttype', () => {
beforeEach(() => {
mockResult = buildFakeResult({
raw: {
filetype: undefined,
objecttype: undefined,
urihash: '',
},
});
});

it('should render a slot with a generic document icon', async () => {
const {getSlot, getAtomicIcon} = await renderComponent();

// Should have a slot for fallback content
expect(getSlot()).toBeDefined();

// The icon should still render with the default 'document' icon
const icon = getAtomicIcon();
expect(icon).toBeDefined();
expect(icon?.getAttribute('icon')).toBe('assets://document');
});
});

describe('license property', () => {
it('should have a license property', async () => {
const {element} = await renderComponent();

expect(element?.license).toBeDefined();
expect(element?.license).toContain('Salesforce');
expect(element?.license).toContain('Creative Commons');
});
});
});
Loading
Loading