Date: Thu, 8 Jan 2026 11:10:15 -0500
Subject: [PATCH 14/18] use msw
---
.../atomic-smart-snippet.new.stories.tsx | 83 +++++++++++--------
1 file changed, 48 insertions(+), 35 deletions(-)
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
index d3d300f6e7c..0c3dfeb8832 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
+++ b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
@@ -1,47 +1,18 @@
+import type {Result} from '@coveo/headless';
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
+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 {events, args, argTypes, template} = getStorybookHelpers(
'atomic-smart-snippet',
{excludeCategories: ['methods']}
);
-const {decorator, play} = wrapInSearchInterface({
- config: {
- search: {
- preprocessSearchResponseMiddleware: (r) => {
- const [result] = r.body.results;
- result.title = 'Manage the Coveo In-Product Experiences (IPX)';
- result.clickUri = 'https://docs.coveo.com/en/3160';
- r.body.questionAnswer = {
- documentId: {
- contentIdKey: 'permanentid',
- contentIdValue: result.raw.permanentid!,
- },
- question: 'Creating an In-Product Experience (IPX)',
- answerSnippet: `
-
- - On the In-Product Experiences page, click Add In-Product Experience.
- - In the Configuration tab, fill the Basic settings section.
- - (Optional) Use the Design and Content access tabs to customize your IPX interface.
- - Click Save.
- - In the Loader snippet panel that appears, you may click Copy to save the loader snippet for your IPX to your clipboard, and then click Save. You can Always retrieve the loader snippet later.
-
-
-
- You're now ready to embed your IPX interface. However, we recommend that you configure query pipelines for your IPX interface before.
-
- `,
- relatedQuestions: [],
- score: 1337,
- };
- return r;
- },
- },
- },
-});
+const {decorator, play} = wrapInSearchInterface();
const meta: Meta = {
component: 'atomic-smart-snippet',
@@ -54,10 +25,52 @@ const meta: Meta = {
actions: {
handles: events,
},
+ msw: {
+ handlers: [...mockSearchApi.handlers],
+ },
},
args,
argTypes,
-
+ beforeEach: async () => {
+ mockSearchApi.searchEndpoint.clear();
+ mockSearchApi.searchEndpoint.mockOnce((response) => {
+ if (!('results' in response)) return response;
+ const [result] = response.results as Result[];
+ return {
+ ...response,
+ results: [
+ {
+ ...result,
+ title: 'Manage the Coveo In-Product Experiences (IPX)',
+ clickUri: 'https://docs.coveo.com/en/3160',
+ },
+ ...response.results.slice(1),
+ ],
+ questionAnswer: {
+ answerFound: true,
+ documentId: {
+ contentIdKey: 'permanentid',
+ contentIdValue: result.raw.permanentid,
+ },
+ question: 'Creating an In-Product Experience (IPX)',
+ answerSnippet: `
+
+ - On the In-Product Experiences page, click Add In-Product Experience.
+ - In the Configuration tab, fill the Basic settings section.
+ - (Optional) Use the Design and Content access tabs to customize your IPX interface.
+ - Click Save.
+ - In the Loader snippet panel that appears, you may click Copy to save the loader snippet for your IPX to your clipboard, and then click Save. You can always retrieve the loader snippet later.
+
+
+ You're now ready to embed your IPX interface. However, we recommend that you configure query pipelines for your IPX interface before.
+
+ `,
+ relatedQuestions: [],
+ score: 1337,
+ },
+ };
+ });
+ },
play,
};
From 8a122b2c954664d98fb37dbbdae9462e94ea6e57 Mon Sep 17 00:00:00 2001
From: ylakhdar
Date: Thu, 8 Jan 2026 12:18:41 -0500
Subject: [PATCH 15/18] fix tests
---
...ic-smart-snippet-expandable-answer.spec.ts | 13 +-
.../atomic-smart-snippet-expandable-answer.ts | 1 +
.../snippet-truncated-answer.ts | 1 +
.../atomic-smart-snippet-suggestions.ts | 1 +
.../atomic-smart-snippet.new.stories.tsx | 6 +-
.../atomic-smart-snippet.spec.ts | 280 +++++++-----------
6 files changed, 126 insertions(+), 176 deletions(-)
diff --git a/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.spec.ts b/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.spec.ts
index 25e104e8ffa..10af031e43f 100644
--- a/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.spec.ts
+++ b/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.spec.ts
@@ -1,7 +1,6 @@
import type {i18n} from 'i18next';
import {html} from 'lit';
import {beforeEach, describe, expect, it} from 'vitest';
-import {page} from 'vitest/browser';
import {fixture} from '@/vitest-utils/testing-helpers/fixture';
import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';
import {AtomicSmartSnippetExpandableAnswer} from './atomic-smart-snippet-expandable-answer';
@@ -202,7 +201,7 @@ describe('atomic-smart-snippet-expandable-answer', () => {
describe('events', () => {
it('should emit expand event when show-more button is clicked', async () => {
- const {element} = await renderComponent({expanded: false});
+ const {element, parts} = await renderComponent({expanded: false});
await setElementHeight(element, 300);
await element.requestUpdate();
@@ -213,14 +212,14 @@ describe('atomic-smart-snippet-expandable-answer', () => {
expandEventFired = true;
});
- const button = page.getByRole('button');
- await button.click();
+ const showMoreButton = parts(element).showMoreButton as HTMLElement;
+ showMoreButton.click();
expect(expandEventFired).toBe(true);
});
it('should emit collapse event when show-less button is clicked', async () => {
- const {element} = await renderComponent({expanded: true});
+ const {element, parts} = await renderComponent({expanded: true});
await setElementHeight(element, 300);
await element.requestUpdate();
@@ -231,8 +230,8 @@ describe('atomic-smart-snippet-expandable-answer', () => {
collapseEventFired = true;
});
- const button = page.getByRole('button');
- await button.click();
+ const showLessButton = parts(element).showLessButton as HTMLElement;
+ showLessButton.click();
expect(collapseEventFired).toBe(true);
});
diff --git a/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.ts b/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.ts
index c1fe771888b..dbdd6fddd54 100644
--- a/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.ts
+++ b/packages/atomic/src/components/common/atomic-smart-snippet-expandable-answer/atomic-smart-snippet-expandable-answer.ts
@@ -13,6 +13,7 @@ import ArrowDown from '@/src/images/arrow-down.svg';
import {listenOnce} from '@/src/utils/event-utils.js';
import styles from './atomic-smart-snippet-expandable-answer.tw.css.js';
import '@/src/components/common/atomic-icon/atomic-icon.js';
+import '@/src/components/common/atomic-smart-snippet-answer/atomic-smart-snippet-answer.js';
// TODO: uncomment when PR #6781 is merged
// import '@/src/components/common/smart-snippets/atomic-smart-snippet-answer/atomic-smart-snippet-answer.js';
diff --git a/packages/atomic/src/components/common/smart-snippets/atomic-smart-snippet/snippet-truncated-answer.ts b/packages/atomic/src/components/common/smart-snippets/atomic-smart-snippet/snippet-truncated-answer.ts
index 0e0546a2d0a..9e34450ad22 100644
--- a/packages/atomic/src/components/common/smart-snippets/atomic-smart-snippet/snippet-truncated-answer.ts
+++ b/packages/atomic/src/components/common/smart-snippets/atomic-smart-snippet/snippet-truncated-answer.ts
@@ -1,6 +1,7 @@
import {html} from 'lit';
import {ifDefined} from 'lit/directives/if-defined.js';
import type {FunctionalComponent} from '@/src/utils/functional-component-utils';
+import '@/src/components/common/atomic-smart-snippet-answer/atomic-smart-snippet-answer.js';
export interface SnippetTruncatedAnswerProps {
answer: string;
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.ts b/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.ts
index 261424434e6..77df7361ce7 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.ts
+++ b/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.ts
@@ -28,6 +28,7 @@ import ArrowDown from '@/src/images/arrow-down.svg';
import ArrowRight from '@/src/images/arrow-right.svg';
import {randomID} from '@/src/utils/utils';
import '@/src/components/common/atomic-icon/atomic-icon';
+import '@/src/components/common/atomic-smart-snippet-answer/atomic-smart-snippet-answer.js';
import styles from './atomic-smart-snippet-suggestions.tw.css';
/**
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
index 0c3dfeb8832..31d049baafb 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
+++ b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.new.stories.tsx
@@ -16,7 +16,7 @@ const {decorator, play} = wrapInSearchInterface();
const meta: Meta = {
component: 'atomic-smart-snippet',
- title: 'Search/SmartSnippet',
+ title: 'Search/Smart Snippet',
id: 'atomic-smart-snippet',
render: (args) => template(args),
decorators: [decorator],
@@ -76,6 +76,4 @@ const meta: Meta = {
export default meta;
-export const Default: Story = {
- name: 'atomic-smart-snippet',
-};
+export const Default: Story = {};
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
index 554aef6ee00..989b255b168 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
+++ b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
@@ -7,7 +7,6 @@ import {
} from '@coveo/headless';
import {html} from 'lit';
import {beforeEach, describe, expect, it, vi} from 'vitest';
-import {page} from 'vitest/browser';
import {renderInAtomicSearchInterface} from '@/vitest-utils/testing-helpers/fixtures/atomic/search/atomic-search-interface-fixture';
import {buildFakeSmartSnippet} from '@/vitest-utils/testing-helpers/fixtures/headless/search/smart-snippet-controller';
import {buildFakeTabManager} from '@/vitest-utils/testing-helpers/fixtures/headless/search/tab-manager-controller';
@@ -75,28 +74,38 @@ describe('atomic-smart-snippet', () => {
selector: 'atomic-smart-snippet',
});
- const parts = (el: AtomicSmartSnippet) => ({
- smartSnippet: el.shadowRoot?.querySelector('[part~="smart-snippet"]'),
- question: el.shadowRoot?.querySelector('[part~="question"]'),
- answer: el.shadowRoot?.querySelector('[part~="answer"]'),
- truncatedAnswer: el.shadowRoot?.querySelector(
+ const getParts = () => ({
+ smartSnippet: element.shadowRoot?.querySelector(
+ '[part~="smart-snippet"]'
+ ),
+ question: element.shadowRoot?.querySelector('[part~="question"]'),
+ answer: element.shadowRoot?.querySelector('atomic-smart-snippet-answer'),
+ truncatedAnswer: element.shadowRoot?.querySelector(
'[part~="truncated-answer"]'
),
- body: el.shadowRoot?.querySelector('[part~="body"]'),
- footer: el.shadowRoot?.querySelector('[part~="footer"]'),
- feedbackBanner: el.shadowRoot?.querySelector('[part~="feedback-banner"]'),
- feedbackLikeButton: el.shadowRoot?.querySelector(
- '[part~="feedback-like-button"]'
+ body: element.shadowRoot?.querySelector('[part~="body"]'),
+ footer: element.shadowRoot?.querySelector('[part~="footer"]'),
+ feedbackBanner: element.shadowRoot?.querySelector(
+ '[part~="feedback-banner"]'
),
- feedbackDislikeButton: el.shadowRoot?.querySelector(
+ feedbackLikeButton: element.shadowRoot?.querySelector(
+ '[part~="feedback-like-button"]'
+ ) as HTMLElement | null,
+ feedbackDislikeButton: element.shadowRoot?.querySelector(
'[part~="feedback-dislike-button"]'
- ),
- feedbackThankYou: el.shadowRoot?.querySelector(
+ ) as HTMLElement | null,
+ feedbackThankYou: element.shadowRoot?.querySelector(
'[part~="feedback-thank-you"]'
),
+ expandableAnswer: element.shadowRoot?.querySelector(
+ 'atomic-smart-snippet-expandable-answer'
+ ),
+ source: element.shadowRoot?.querySelector('atomic-smart-snippet-source'),
+ sourceUrl: element.shadowRoot?.querySelector('[part~="source-url"]'),
+ sourceTitle: element.shadowRoot?.querySelector('[part~="source-title"]'),
});
- return {element, parts};
+ return {element, getParts};
};
describe('when controller is initialized', () => {
@@ -117,66 +126,55 @@ describe('atomic-smart-snippet', () => {
});
it('should render the smart snippet', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
- await expect.element(parts(element).smartSnippet!).toBeInTheDocument();
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().smartSnippet).toBeInTheDocument();
});
it('should render the question', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
- const question = parts(element).question!;
- await expect.element(question).toBeInTheDocument();
+ const {getParts} = await renderAtomicSmartSnippet();
+ const question = getParts().question!;
+ expect(question).toBeInTheDocument();
expect(question.textContent?.trim()).toBe(
mockedSmartSnippet.state.question
);
});
it('should render the expandable answer when snippetMaximumHeight is undefined', async () => {
- const {element} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {snippetMaximumHeight: undefined},
});
- const expandableAnswer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-expandable-answer'
- );
- await expect.element(expandableAnswer!).toBeInTheDocument();
+ expect(getParts().expandableAnswer).toBeInTheDocument();
});
it('should render the truncated answer when snippetMaximumHeight is defined', async () => {
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {snippetMaximumHeight: 200},
});
- await expect.element(parts(element).truncatedAnswer!).toBeInTheDocument();
+ expect(getParts().truncatedAnswer).toBeInTheDocument();
});
it('should render the footer', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
- await expect.element(parts(element).footer!).toBeInTheDocument();
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().footer).toBeInTheDocument();
});
it('should render the source when source is present', async () => {
- const {element} = await renderAtomicSmartSnippet();
- const source = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-source'
- );
- await expect.element(source!).toBeInTheDocument();
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().source).toBeInTheDocument();
});
it('should render the feedback banner', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
- await expect.element(parts(element).feedbackBanner!).toBeInTheDocument();
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().feedbackBanner).toBeInTheDocument();
});
// TODO: Enable when atomic-smart-snippet-source is migrated to Lit
it.skip('should render source url and title with correct href', async () => {
- const {element} = await renderAtomicSmartSnippet();
- const sourceUrl = element.shadowRoot?.querySelector(
- '[part~="source-url"]'
- );
- const sourceTitle = element.shadowRoot?.querySelector(
- '[part~="source-title"]'
- );
+ const {getParts} = await renderAtomicSmartSnippet();
+ const {sourceUrl, sourceTitle} = getParts();
- await expect.element(sourceUrl!).toBeInTheDocument();
- await expect.element(sourceTitle!).toBeInTheDocument();
+ expect(sourceUrl).toBeInTheDocument();
+ expect(sourceTitle).toBeInTheDocument();
expect(sourceUrl?.getAttribute('href')).toBe(
mockedSmartSnippet.state.source?.clickUri
);
@@ -186,71 +184,53 @@ describe('atomic-smart-snippet', () => {
});
});
- describe('when answer is not found', () => {
- beforeEach(() => {
- mockedSmartSnippet.state.answerFound = false;
- });
-
- it('should not render the smart snippet', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
- await expect
- .element(parts(element).smartSnippet!)
- .not.toBeInTheDocument();
- });
+ it('should not render the smart snippet when answer is not found', async () => {
+ mockedSmartSnippet.state.answerFound = false;
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().smartSnippet).not.toBeInTheDocument();
});
- describe('when source is not present', () => {
- beforeEach(() => {
- mockedSmartSnippet.state.source = null;
- });
-
- it('should not render the source', async () => {
- const {element} = await renderAtomicSmartSnippet();
- const source = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-source'
- );
- expect(source).toBeNull();
- });
+ it('should not render the source when source is not present', async () => {
+ // @ts-expect-error: Testing null source
+ mockedSmartSnippet.state.source = null;
+ const {getParts} = await renderAtomicSmartSnippet();
+ expect(getParts().source).toBeNull();
});
describe('tab filtering', () => {
describe('when tabsIncluded is set', () => {
it('should render when current tab is included', async () => {
mockedTabManager.state.activeTab = 'tab1';
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {tabsIncluded: ['tab1', 'tab2']},
});
- await expect.element(parts(element).smartSnippet!).toBeInTheDocument();
+ expect(getParts().smartSnippet).toBeInTheDocument();
});
it('should not render when current tab is not included', async () => {
mockedTabManager.state.activeTab = 'tab3';
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {tabsIncluded: ['tab1', 'tab2']},
});
- await expect
- .element(parts(element).smartSnippet!)
- .not.toBeInTheDocument();
+ expect(getParts().smartSnippet).not.toBeInTheDocument();
});
});
describe('when tabsExcluded is set', () => {
it('should not render when current tab is excluded', async () => {
mockedTabManager.state.activeTab = 'tab1';
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {tabsExcluded: ['tab1', 'tab2']},
});
- await expect
- .element(parts(element).smartSnippet!)
- .not.toBeInTheDocument();
+ expect(getParts().smartSnippet).not.toBeInTheDocument();
});
it('should render when current tab is not excluded', async () => {
mockedTabManager.state.activeTab = 'tab3';
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {getParts} = await renderAtomicSmartSnippet({
props: {tabsExcluded: ['tab1', 'tab2']},
});
- await expect.element(parts(element).smartSnippet!).toBeInTheDocument();
+ expect(getParts().smartSnippet).toBeInTheDocument();
});
});
});
@@ -259,23 +239,23 @@ describe('atomic-smart-snippet', () => {
// TODO: Enable when feedback button selectors are fixed
it.skip('should call smartSnippet.like() when like button is clicked', async () => {
const likeSpy = vi.spyOn(mockedSmartSnippet, 'like');
- await renderAtomicSmartSnippet();
- await page.getByRole('radiogroup').getByText('yes').click();
+ const {getParts} = await renderAtomicSmartSnippet();
+ getParts().feedbackLikeButton?.click();
expect(likeSpy).toHaveBeenCalled();
});
// TODO: Enable when feedback button selectors are fixed
it.skip('should call smartSnippet.dislike() when dislike button is clicked', async () => {
const dislikeSpy = vi.spyOn(mockedSmartSnippet, 'dislike');
- await renderAtomicSmartSnippet();
- await page.getByRole('radiogroup').getByText('no').click();
+ const {getParts} = await renderAtomicSmartSnippet();
+ getParts().feedbackDislikeButton?.click();
expect(dislikeSpy).toHaveBeenCalled();
});
// TODO: Enable when feedback button selectors are fixed
it.skip('should load modal when dislike button is clicked', async () => {
- const {element} = await renderAtomicSmartSnippet();
- await page.getByRole('radiogroup').getByText('no').click();
+ const {element, getParts} = await renderAtomicSmartSnippet();
+ getParts().feedbackDislikeButton?.click();
await vi.waitFor(() => {
const modal = element
@@ -288,51 +268,43 @@ describe('atomic-smart-snippet', () => {
// TODO: Enable when feedback button selectors are fixed
it.skip('should show thank you message after liking', async () => {
mockedSmartSnippet.state.liked = false;
- const {element, parts} = await renderAtomicSmartSnippet();
+ const {element, getParts} = await renderAtomicSmartSnippet();
- await page.getByRole('radiogroup').getByText('yes').click();
+ getParts().feedbackLikeButton?.click();
mockedSmartSnippet.state.liked = true;
element.requestUpdate();
await element.updateComplete;
- await expect
- .element(parts(element).feedbackThankYou!)
- .toBeInTheDocument();
+ expect(getParts().feedbackThankYou).toBeInTheDocument();
});
// TODO: Enable when feedback button selectors are fixed
it.skip('should show thank you message after disliking', async () => {
mockedSmartSnippet.state.disliked = false;
- const {element, parts} = await renderAtomicSmartSnippet();
+ const {element, getParts} = await renderAtomicSmartSnippet();
- await page.getByRole('radiogroup').getByText('no').click();
+ getParts().feedbackDislikeButton?.click();
mockedSmartSnippet.state.disliked = true;
element.requestUpdate();
await element.updateComplete;
- await expect
- .element(parts(element).feedbackThankYou!)
- .toBeInTheDocument();
+ expect(getParts().feedbackThankYou).toBeInTheDocument();
});
it('should hide thank you message when liked state changes to false', async () => {
mockedSmartSnippet.state.liked = true;
- const {element, parts} = await renderAtomicSmartSnippet();
+ const {element, getParts} = await renderAtomicSmartSnippet();
element.requestUpdate();
await element.updateComplete;
- await expect
- .element(parts(element).feedbackThankYou!)
- .toBeInTheDocument();
+ expect(getParts().feedbackThankYou).toBeInTheDocument();
mockedSmartSnippet.state.liked = false;
element.requestUpdate();
await element.updateComplete;
- await expect
- .element(parts(element).feedbackThankYou!)
- .not.toBeInTheDocument();
+ expect(getParts().feedbackThankYou).not.toBeInTheDocument();
});
});
@@ -389,65 +361,53 @@ describe('atomic-smart-snippet', () => {
describe('expandable answer integration', () => {
it('should call smartSnippet.expand() when expand event is dispatched', async () => {
const expandSpy = vi.spyOn(mockedSmartSnippet, 'expand');
- const {element} = await renderAtomicSmartSnippet();
- const expandableAnswer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-expandable-answer'
- );
- expandableAnswer?.dispatchEvent(new CustomEvent('expand'));
+ const {getParts} = await renderAtomicSmartSnippet();
+ getParts().expandableAnswer?.dispatchEvent(new CustomEvent('expand'));
expect(expandSpy).toHaveBeenCalled();
});
it('should call smartSnippet.collapse() when collapse event is dispatched', async () => {
const collapseSpy = vi.spyOn(mockedSmartSnippet, 'collapse');
- const {element} = await renderAtomicSmartSnippet();
- const expandableAnswer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-expandable-answer'
- );
- expandableAnswer?.dispatchEvent(new CustomEvent('collapse'));
+ const {getParts} = await renderAtomicSmartSnippet();
+ getParts().expandableAnswer?.dispatchEvent(new CustomEvent('collapse'));
expect(collapseSpy).toHaveBeenCalled();
});
});
describe('props', () => {
it('should pass headingLevel prop to question renderer', async () => {
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {element, getParts} = await renderAtomicSmartSnippet({
props: {headingLevel: 2},
});
expect(element.headingLevel).toBe(2);
- await expect.element(parts(element).question!).toBeInTheDocument();
+ expect(getParts().question).toBeInTheDocument();
});
it('should use heading level 0 when no heading level is specified', async () => {
- const {element, parts} = await renderAtomicSmartSnippet({
+ const {element, getParts} = await renderAtomicSmartSnippet({
props: {headingLevel: 0},
});
expect(element.headingLevel).toBe(0);
- await expect.element(parts(element).question!).toBeInTheDocument();
+ expect(getParts().question).toBeInTheDocument();
- const question = parts(element).question!;
+ const question = getParts().question!;
expect(question.tagName).toBe('DIV');
});
it('should pass maximumHeight prop to expandable answer', async () => {
- const {element} = await renderAtomicSmartSnippet({
+ const {element, getParts} = await renderAtomicSmartSnippet({
props: {maximumHeight: 300},
});
- const expandableAnswer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-expandable-answer'
- );
expect(element.maximumHeight).toBe(300);
- await expect.element(expandableAnswer!).toBeInTheDocument();
+ expect(getParts().expandableAnswer).toBeInTheDocument();
});
it('should pass collapsedHeight prop to expandable answer', async () => {
- const {element} = await renderAtomicSmartSnippet({
+ const {element, getParts} = await renderAtomicSmartSnippet({
props: {collapsedHeight: 150},
});
- const expandableAnswer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-expandable-answer'
- );
expect(element.collapsedHeight).toBe(150);
- await expect.element(expandableAnswer!).toBeInTheDocument();
+ expect(getParts().expandableAnswer).toBeInTheDocument();
});
it('should accept snippetStyle prop', async () => {
@@ -469,67 +429,57 @@ describe('atomic-smart-snippet', () => {
});
});
- describe('event listener cleanup', () => {
- it('should remove event listeners when disconnected', async () => {
- const {element} = await renderAtomicSmartSnippet();
- const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');
- element.disconnectedCallback();
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
- 'selectInlineLink',
- expect.any(Function)
- );
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
- 'beginDelayedSelectInlineLink',
- expect.any(Function)
- );
- expect(removeEventListenerSpy).toHaveBeenCalledWith(
- 'cancelPendingSelectInlineLink',
- expect.any(Function)
- );
- });
+ it('should remove event listeners when disconnected', async () => {
+ const {element} = await renderAtomicSmartSnippet();
+ const removeEventListenerSpy = vi.spyOn(element, 'removeEventListener');
+ element.disconnectedCallback();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'selectInlineLink',
+ expect.any(Function)
+ );
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'beginDelayedSelectInlineLink',
+ expect.any(Function)
+ );
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ 'cancelPendingSelectInlineLink',
+ expect.any(Function)
+ );
});
describe('dynamic updates', () => {
it('should update question when smartSnippetState changes', async () => {
- const {element, parts} = await renderAtomicSmartSnippet();
+ const {element, getParts} = await renderAtomicSmartSnippet();
const newQuestion = 'What is the answer to everything?';
mockedSmartSnippet.state.question = newQuestion;
element.requestUpdate();
await element.updateComplete;
- const question = parts(element).question!;
+ const question = getParts().question!;
expect(question.textContent?.trim()).toBe(newQuestion);
});
it('should update answer when smartSnippetState changes', async () => {
- const {element} = await renderAtomicSmartSnippet();
+ const {element, getParts} = await renderAtomicSmartSnippet();
const newAnswer = 'New answer content
';
mockedSmartSnippet.state.answer = newAnswer;
element.requestUpdate();
await element.updateComplete;
- const answer = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-answer'
- );
- await expect.element(answer!).toBeInTheDocument();
+ expect(getParts().answer).toBeInTheDocument();
});
});
- describe('slot attributes', () => {
- it('should pass slot attributes to source anchor', async () => {
- const {element} = await renderAtomicSmartSnippet();
+ it('should pass slot attributes to source anchor', async () => {
+ const {element, getParts} = await renderAtomicSmartSnippet();
- const slotElement = document.createElement('a');
- slotElement.setAttribute('slot', 'source-anchor-attributes');
- slotElement.setAttribute('target', '_blank');
- element.appendChild(slotElement);
+ const slotElement = document.createElement('a');
+ slotElement.setAttribute('slot', 'source-anchor-attributes');
+ slotElement.setAttribute('target', '_blank');
+ element.appendChild(slotElement);
- const source = element.shadowRoot?.querySelector(
- 'atomic-smart-snippet-source'
- );
- await expect.element(source!).toBeInTheDocument();
- });
+ expect(getParts().source).toBeInTheDocument();
});
});
From 6d70b4681ab42cd94d56d2308778f3663b10027f Mon Sep 17 00:00:00 2001
From: ylakhdar
Date: Thu, 8 Jan 2026 12:51:20 -0500
Subject: [PATCH 16/18] move smart snippet suggestion story one level up
---
.../atomic-smart-snippet-suggestions.new.stories.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.new.stories.tsx b/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.new.stories.tsx
index 363ff0da8fe..39331bf7821 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.new.stories.tsx
+++ b/packages/atomic/src/components/search/atomic-smart-snippet-suggestions/atomic-smart-snippet-suggestions.new.stories.tsx
@@ -162,7 +162,7 @@ const {decorator, play} = wrapInSearchInterface({
const meta: Meta = {
component: 'atomic-smart-snippet-suggestions',
- title: 'Search/SmartSnippet/SmartSnippetSuggestions',
+ title: 'Search/Smart Snippet Suggestions',
id: 'atomic-smart-snippet-suggestions',
render: (args) => template(args),
decorators: [decorator],
From 169349a74fccd0cf38d1d8e798cda250b593e44e Mon Sep 17 00:00:00 2001
From: ylakhdar
Date: Mon, 12 Jan 2026 15:29:49 -0500
Subject: [PATCH 17/18] corrections
---
packages/atomic/src/components.d.ts | 125 ------------------
.../atomic-smart-snippet.spec.ts | 2 +-
.../atomic-smart-snippet.ts | 8 +-
.../atomic-smart-snippet.tw.css.ts | 1 +
.../atomic/src/components/search/index.ts | 1 +
.../src/components/search/lazy-index.ts | 2 +
6 files changed, 10 insertions(+), 129 deletions(-)
diff --git a/packages/atomic/src/components.d.ts b/packages/atomic/src/components.d.ts
index e7cd1c2abd1..cc418d4176e 100644
--- a/packages/atomic/src/components.d.ts
+++ b/packages/atomic/src/components.d.ts
@@ -747,49 +747,6 @@ export namespace Components {
*/
"store"?: RecsStore;
}
- /**
- * The `atomic-smart-snippet` component displays the excerpt of a document that would be most likely to answer a particular query.
- * You can style the snippet by inserting a template element as follows:
- * ```html
- *
- *
- *
- *
- *
- * ```
- */
- interface AtomicSmartSnippet {
- /**
- * When the answer is partly hidden, how much of its height (in pixels) should be visible.
- */
- "collapsedHeight": number;
- /**
- * The [heading level](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) to use for the question at the top of the snippet, from 1 to 5.
- */
- "headingLevel": number;
- /**
- * The maximum height (in pixels) a snippet can have before the component truncates it and displays a "show more" button.
- */
- "maximumHeight": number;
- "snippetCollapsedHeight"?: number;
- "snippetMaximumHeight"?: number;
- /**
- * Sets the style of the snippet. Example: ```ts smartSnippet.snippetStyle = ` b { color: blue; } `; ```
- */
- "snippetStyle"?: string;
- /**
- * The tabs on which this smart snippet must not be displayed. This property should not be used at the same time as `tabs-included`. Set this property as a stringified JSON array, for example: ```html ``` If you don't set this property, the smart snippet can be displayed on any tab. Otherwise, the smart snippet won't be displayed on any of the specified tabs.
- */
- "tabsExcluded": string[] | string;
- /**
- * The tabs on which the smart snippet can be displayed. This property should not be used at the same time as `tabs-excluded`. Set this property as a stringified JSON array, for example: ```html ``` If you don't set this property, the smart snippet can be displayed on any tab. Otherwise, the smart snippet can only be displayed on the specified tabs.
- */
- "tabsIncluded": string[] | string;
- }
/**
* @deprecated Use `atomic-facet-date-input` instead. This component is meant to be used with Stencil components only.
* Internal component made to be integrated in a TimeframeFacet.
@@ -1277,27 +1234,6 @@ declare global {
prototype: HTMLAtomicRecsResultElement;
new (): HTMLAtomicRecsResultElement;
};
- /**
- * The `atomic-smart-snippet` component displays the excerpt of a document that would be most likely to answer a particular query.
- * You can style the snippet by inserting a template element as follows:
- * ```html
- *
- *
- *
- *
- *
- * ```
- */
- interface HTMLAtomicSmartSnippetElement extends Components.AtomicSmartSnippet, HTMLStencilElement {
- }
- var HTMLAtomicSmartSnippetElement: {
- prototype: HTMLAtomicSmartSnippetElement;
- new (): HTMLAtomicSmartSnippetElement;
- };
interface HTMLAtomicStencilFacetDateInputElementEventMap {
"atomic/dateInputApply": any;
}
@@ -1380,7 +1316,6 @@ declare global {
"atomic-quickview-modal": HTMLAtomicQuickviewModalElement;
"atomic-recs-list": HTMLAtomicRecsListElement;
"atomic-recs-result": HTMLAtomicRecsResultElement;
- "atomic-smart-snippet": HTMLAtomicSmartSnippetElement;
"atomic-stencil-facet-date-input": HTMLAtomicStencilFacetDateInputElement;
"atomic-suggestion-renderer": HTMLAtomicSuggestionRendererElement;
"atomic-timeframe-facet": HTMLAtomicTimeframeFacetElement;
@@ -2072,49 +2007,6 @@ declare namespace LocalJSX {
*/
"store"?: RecsStore;
}
- /**
- * The `atomic-smart-snippet` component displays the excerpt of a document that would be most likely to answer a particular query.
- * You can style the snippet by inserting a template element as follows:
- * ```html
- *
- *
- *
- *
- *
- * ```
- */
- interface AtomicSmartSnippet {
- /**
- * When the answer is partly hidden, how much of its height (in pixels) should be visible.
- */
- "collapsedHeight"?: number;
- /**
- * The [heading level](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) to use for the question at the top of the snippet, from 1 to 5.
- */
- "headingLevel"?: number;
- /**
- * The maximum height (in pixels) a snippet can have before the component truncates it and displays a "show more" button.
- */
- "maximumHeight"?: number;
- "snippetCollapsedHeight"?: number;
- "snippetMaximumHeight"?: number;
- /**
- * Sets the style of the snippet. Example: ```ts smartSnippet.snippetStyle = ` b { color: blue; } `; ```
- */
- "snippetStyle"?: string;
- /**
- * The tabs on which this smart snippet must not be displayed. This property should not be used at the same time as `tabs-included`. Set this property as a stringified JSON array, for example: ```html ``` If you don't set this property, the smart snippet can be displayed on any tab. Otherwise, the smart snippet won't be displayed on any of the specified tabs.
- */
- "tabsExcluded"?: string[] | string;
- /**
- * The tabs on which the smart snippet can be displayed. This property should not be used at the same time as `tabs-excluded`. Set this property as a stringified JSON array, for example: ```html ``` If you don't set this property, the smart snippet can be displayed on any tab. Otherwise, the smart snippet can only be displayed on the specified tabs.
- */
- "tabsIncluded"?: string[] | string;
- }
/**
* @deprecated Use `atomic-facet-date-input` instead. This component is meant to be used with Stencil components only.
* Internal component made to be integrated in a TimeframeFacet.
@@ -2248,7 +2140,6 @@ declare namespace LocalJSX {
"atomic-quickview-modal": AtomicQuickviewModal;
"atomic-recs-list": AtomicRecsList;
"atomic-recs-result": AtomicRecsResult;
- "atomic-smart-snippet": AtomicSmartSnippet;
"atomic-stencil-facet-date-input": AtomicStencilFacetDateInput;
"atomic-suggestion-renderer": AtomicSuggestionRenderer;
"atomic-timeframe-facet": AtomicTimeframeFacet;
@@ -2334,22 +2225,6 @@ declare module "@stencil/core" {
* The `atomic-recs-result` component is used internally by the `atomic-recs-list` component.
*/
"atomic-recs-result": LocalJSX.AtomicRecsResult & JSXBase.HTMLAttributes;
- /**
- * The `atomic-smart-snippet` component displays the excerpt of a document that would be most likely to answer a particular query.
- * You can style the snippet by inserting a template element as follows:
- * ```html
- *
- *
- *
- *
- *
- * ```
- */
- "atomic-smart-snippet": LocalJSX.AtomicSmartSnippet & JSXBase.HTMLAttributes;
/**
* @deprecated Use `atomic-facet-date-input` instead. This component is meant to be used with Stencil components only.
* Internal component made to be integrated in a TimeframeFacet.
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
index 989b255b168..7b7a459665f 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
+++ b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.spec.ts
@@ -468,7 +468,7 @@ describe('atomic-smart-snippet', () => {
element.requestUpdate();
await element.updateComplete;
- expect(getParts().answer).toBeInTheDocument();
+ expect(getParts().expandableAnswer).toBeInTheDocument();
});
});
diff --git a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.ts b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.ts
index c65e67ec24a..8ce64c5e584 100644
--- a/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.ts
+++ b/packages/atomic/src/components/search/atomic-smart-snippet/atomic-smart-snippet.ts
@@ -30,6 +30,8 @@ import {withTailwindStyles} from '@/src/decorators/with-tailwind-styles';
import {shouldDisplayOnCurrentTab} from '@/src/utils/tab-utils';
import {randomID} from '@/src/utils/utils';
import styles from './atomic-smart-snippet.tw.css';
+import '@/src/components/common/atomic-smart-snippet-source/atomic-smart-snippet-source';
+import type {AtomicSmartSnippetFeedbackModal} from '@/src/components/search/atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.js';
/**
* The `atomic-smart-snippet` component displays the excerpt of a document that would be most likely to answer a particular query.
@@ -95,7 +97,7 @@ export class AtomicSmartSnippet
@state() public feedbackSent = false;
#id!: string;
- private modalRef?: HTMLAtomicSmartSnippetFeedbackModalElement;
+ private modalRef?: AtomicSmartSnippetFeedbackModal;
/**
* The [heading level](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) to use for the question at the top of the snippet, from 1 to 5.
@@ -242,7 +244,7 @@ export class AtomicSmartSnippet
}
private setModalRef(ref: HTMLElement) {
- this.modalRef = ref as HTMLAtomicSmartSnippetFeedbackModalElement;
+ this.modalRef = ref as AtomicSmartSnippetFeedbackModal;
}
private setFeedbackSent(isSent: boolean) {
@@ -335,7 +337,7 @@ export class AtomicSmartSnippet
source,
() => html`
+ await import('./atomic-smart-snippet/atomic-smart-snippet.js'),
'atomic-smart-snippet-feedback-modal': async () =>
await import(
'./atomic-smart-snippet-feedback-modal/atomic-smart-snippet-feedback-modal.js'
From a312fb91af4736e352ab9ed6bb923df6404fb37d Mon Sep 17 00:00:00 2001
From: "developer-experience-bot[bot]"
<91079284+developer-experience-bot[bot]@users.noreply.github.com>
Date: Mon, 12 Jan 2026 20:38:36 +0000
Subject: [PATCH 18/18] Add generated files
---
packages/atomic-react/src/components/search/components.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/packages/atomic-react/src/components/search/components.ts b/packages/atomic-react/src/components/search/components.ts
index 821c104fbb0..ca059ded5db 100644
--- a/packages/atomic-react/src/components/search/components.ts
+++ b/packages/atomic-react/src/components/search/components.ts
@@ -81,6 +81,7 @@ import {
AtomicSearchLayout as LitAtomicSearchLayout,
AtomicSegmentedFacet as LitAtomicSegmentedFacet,
AtomicSegmentedFacetScrollable as LitAtomicSegmentedFacetScrollable,
+ AtomicSmartSnippet as LitAtomicSmartSnippet,
AtomicSmartSnippetFeedbackModal as LitAtomicSmartSnippetFeedbackModal,
AtomicSmartSnippetSuggestions as LitAtomicSmartSnippetSuggestions,
AtomicSortDropdown as LitAtomicSortDropdown,
@@ -586,6 +587,12 @@ export const AtomicSegmentedFacetScrollable = createComponent({
elementClass: LitAtomicSegmentedFacetScrollable,
});
+export const AtomicSmartSnippet = createComponent({
+ tagName: 'atomic-smart-snippet',
+ react: React,
+ elementClass: LitAtomicSmartSnippet,
+});
+
export const AtomicSmartSnippetFeedbackModal = createComponent({
tagName: 'atomic-smart-snippet-feedback-modal',
react: React,