Skip to content

Commit eae59d3

Browse files
refactor: migrate SmartSnippetSuggestions Functional Components (#6599)
Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: alexprudhomme <[email protected]>
1 parent 99e999e commit eae59d3

File tree

15 files changed

+533
-2
lines changed

15 files changed

+533
-2
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {html} from 'lit';
2+
import {describe, expect, it} from 'vitest';
3+
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
4+
import {
5+
type AnswerAndSourceWrapperProps,
6+
renderAnswerAndSourceWrapper,
7+
} from './answer-and-source-wrapper';
8+
9+
describe('#renderAnswerAndSourceWrapper', () => {
10+
const renderComponent = async (
11+
props: Partial<AnswerAndSourceWrapperProps> = {},
12+
children = html`<div>Answer Content</div>`
13+
) => {
14+
const element = await renderFunctionFixture(
15+
html`${renderAnswerAndSourceWrapper({
16+
props: {
17+
id: 'answer-1',
18+
...props,
19+
},
20+
})(children)}`
21+
);
22+
23+
return element.querySelector('div') as HTMLElement;
24+
};
25+
26+
it('should render a div element', async () => {
27+
const wrapper = await renderComponent();
28+
expect(wrapper).toBeInTheDocument();
29+
expect(wrapper.tagName.toLowerCase()).toBe('div');
30+
});
31+
32+
it('should render with answer-and-source part', async () => {
33+
const wrapper = await renderComponent();
34+
expect(wrapper.getAttribute('part')).toBe('answer-and-source');
35+
});
36+
37+
it('should render with correct id', async () => {
38+
const wrapper = await renderComponent({id: 'custom-id'});
39+
expect(wrapper.getAttribute('id')).toBe('custom-id');
40+
});
41+
42+
it('should render children', async () => {
43+
const wrapper = await renderComponent({}, html`<p>Custom Answer</p>`);
44+
expect(wrapper.textContent).toContain('Custom Answer');
45+
});
46+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {html} from 'lit';
2+
import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils';
3+
4+
export interface AnswerAndSourceWrapperProps {
5+
/**
6+
* The ID of the answer.
7+
*/
8+
id: string;
9+
}
10+
11+
export const renderAnswerAndSourceWrapper: FunctionalComponentWithChildren<
12+
AnswerAndSourceWrapperProps
13+
> =
14+
({props}) =>
15+
(children) => {
16+
return html`<div
17+
part="answer-and-source"
18+
class="pr-6 pb-6 pl-10"
19+
id=${props.id}
20+
>
21+
${children}
22+
</div>`;
23+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type {i18n} from 'i18next';
2+
import {html} from 'lit';
3+
import {beforeAll, describe, expect, it} from 'vitest';
4+
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
5+
import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';
6+
import {type FooterProps, renderFooter} from './footer';
7+
8+
describe('#renderFooter', () => {
9+
let i18n: i18n;
10+
11+
beforeAll(async () => {
12+
i18n = await createTestI18n();
13+
});
14+
15+
const renderComponent = async (
16+
props: Partial<FooterProps> = {},
17+
children = html`<a href="#">Source</a>`
18+
) => {
19+
const element = await renderFunctionFixture(
20+
html`${renderFooter({
21+
props: {
22+
i18n,
23+
...props,
24+
},
25+
})(children)}`
26+
);
27+
28+
return element.querySelector('footer') as HTMLElement;
29+
};
30+
31+
it('should render a footer element', async () => {
32+
const footer = await renderComponent();
33+
expect(footer).toBeInTheDocument();
34+
expect(footer.tagName.toLowerCase()).toBe('footer');
35+
});
36+
37+
it('should render with footer part', async () => {
38+
const footer = await renderComponent();
39+
expect(footer.getAttribute('part')).toBe('footer');
40+
});
41+
42+
it('should have correct aria-label with translated text', async () => {
43+
const footer = await renderComponent();
44+
expect(footer.getAttribute('aria-label')).toBe('Source of the answer');
45+
});
46+
47+
it('should render children', async () => {
48+
const footer = await renderComponent(
49+
{},
50+
html`<a href="#">Custom Source</a>`
51+
);
52+
expect(footer.textContent).toContain('Custom Source');
53+
});
54+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type {i18n} from 'i18next';
2+
import {html} from 'lit';
3+
import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils';
4+
5+
export interface FooterProps {
6+
/**
7+
* The i18n instance for translations.
8+
*/
9+
i18n: i18n;
10+
}
11+
12+
export const renderFooter: FunctionalComponentWithChildren<FooterProps> =
13+
({props}) =>
14+
(children) => {
15+
return html`<footer
16+
part="footer"
17+
aria-label=${props.i18n.t('smart-snippet-source')}
18+
>
19+
${children}
20+
</footer>`;
21+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {describe, expect, it} from 'vitest';
2+
import {getQuestionPart} from './get-question-part';
3+
4+
describe('#getQuestionPart', () => {
5+
it('should return collapsed part when expanded is false', () => {
6+
expect(getQuestionPart('button', false)).toBe('question-button-collapsed');
7+
expect(getQuestionPart('text', false)).toBe('question-text-collapsed');
8+
expect(getQuestionPart('icon', false)).toBe('question-icon-collapsed');
9+
});
10+
11+
it('should return expanded part when expanded is true', () => {
12+
expect(getQuestionPart('button', true)).toBe('question-button-expanded');
13+
expect(getQuestionPart('text', true)).toBe('question-text-expanded');
14+
expect(getQuestionPart('icon', true)).toBe('question-icon-expanded');
15+
});
16+
17+
it('should handle various base values', () => {
18+
expect(getQuestionPart('custom', false)).toBe('question-custom-collapsed');
19+
expect(getQuestionPart('another', true)).toBe('question-another-expanded');
20+
});
21+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Helper function to get the question part name based on the base and expanded state.
3+
*/
4+
export const getQuestionPart = (base: string, expanded: boolean): string =>
5+
`question-${base}-${expanded ? 'expanded' : 'collapsed'}`;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {html} from 'lit';
2+
import {describe, expect, it} from 'vitest';
3+
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
4+
import {
5+
type QuestionWrapperProps,
6+
renderQuestionWrapper,
7+
} from './question-wrapper';
8+
9+
describe('#renderQuestionWrapper', () => {
10+
const renderComponent = async (
11+
props: Partial<QuestionWrapperProps> = {},
12+
children = html`<div>Question Content</div>`
13+
) => {
14+
const element = await renderFunctionFixture(
15+
html`${renderQuestionWrapper({
16+
props: {
17+
expanded: false,
18+
key: 'question-1',
19+
...props,
20+
},
21+
})(children)}`
22+
);
23+
24+
return element.querySelector('li') as HTMLElement;
25+
};
26+
27+
it('should render an li element', async () => {
28+
const wrapper = await renderComponent();
29+
expect(wrapper).toBeInTheDocument();
30+
expect(wrapper.tagName.toLowerCase()).toBe('li');
31+
});
32+
33+
it('should render with collapsed part when not expanded', async () => {
34+
const wrapper = await renderComponent({expanded: false});
35+
expect(wrapper.getAttribute('part')).toBe('question-answer-collapsed');
36+
});
37+
38+
it('should render with expanded part when expanded', async () => {
39+
const wrapper = await renderComponent({expanded: true});
40+
expect(wrapper.getAttribute('part')).toBe('question-answer-expanded');
41+
});
42+
43+
it('should render children inside article', async () => {
44+
const wrapper = await renderComponent({}, html`<div>Custom Content</div>`);
45+
const article = wrapper.querySelector('article');
46+
expect(article).toBeInTheDocument();
47+
expect(article?.textContent).toContain('Custom Content');
48+
});
49+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {html} from 'lit';
2+
import {keyed} from 'lit/directives/keyed.js';
3+
import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils';
4+
5+
export interface QuestionWrapperProps {
6+
/**
7+
* Whether the question is expanded.
8+
*/
9+
expanded: boolean;
10+
/**
11+
* The key for the question.
12+
*/
13+
key: string;
14+
}
15+
16+
export const renderQuestionWrapper: FunctionalComponentWithChildren<
17+
QuestionWrapperProps
18+
> =
19+
({props}) =>
20+
(children) => {
21+
return html`${keyed(
22+
props.key,
23+
html`<li
24+
part=${`question-answer-${props.expanded ? 'expanded' : 'collapsed'}`}
25+
class="flex flex-col"
26+
>
27+
<article class="contents">${children}</article>
28+
</li>`
29+
)}`;
30+
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {html} from 'lit';
2+
import {describe, expect, it, vi} from 'vitest';
3+
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
4+
import {type QuestionProps, renderQuestion} from './question';
5+
6+
describe('#renderQuestion', () => {
7+
const renderComponent = async (
8+
props: Partial<QuestionProps> = {},
9+
children = html``
10+
) => {
11+
const element = await renderFunctionFixture(
12+
html`${renderQuestion({
13+
props: {
14+
ariaControls: 'answer-1',
15+
expanded: false,
16+
onClick: vi.fn(),
17+
question: 'What is the answer?',
18+
...props,
19+
},
20+
})(children)}`
21+
);
22+
23+
return element.querySelector('button') as HTMLButtonElement;
24+
};
25+
26+
it('should render a button', async () => {
27+
const button = await renderComponent();
28+
expect(button).toBeInTheDocument();
29+
});
30+
31+
it('should render with collapsed button part when not expanded', async () => {
32+
const button = await renderComponent({expanded: false});
33+
expect(button.getAttribute('part')).toBe('question-button-collapsed');
34+
});
35+
36+
it('should render with expanded button part when expanded', async () => {
37+
const button = await renderComponent({expanded: true});
38+
expect(button.getAttribute('part')).toBe('question-button-expanded');
39+
});
40+
41+
it('should render heading with question text', async () => {
42+
const button = await renderComponent({question: 'What is AI?'});
43+
const headingElement = button.querySelector('[part*="question-text"]');
44+
expect(headingElement?.textContent).toContain('What is AI?');
45+
});
46+
47+
it('should render heading with correct level when headingLevel is provided', async () => {
48+
const button = await renderComponent({
49+
headingLevel: 2,
50+
question: 'Question?',
51+
});
52+
const heading = button.querySelector('h3');
53+
expect(heading).toBeInTheDocument();
54+
});
55+
56+
it('should render heading with collapsed text part when not expanded', async () => {
57+
const button = await renderComponent({expanded: false});
58+
const headingElement = button.querySelector('[part*="question-text"]');
59+
expect(headingElement?.getAttribute('part')).toBe(
60+
'question-text-collapsed'
61+
);
62+
});
63+
64+
it('should render heading with expanded text part when expanded', async () => {
65+
const button = await renderComponent({expanded: true});
66+
const headingElement = button.querySelector('[part*="question-text"]');
67+
expect(headingElement?.getAttribute('part')).toBe('question-text-expanded');
68+
});
69+
70+
it('should have aria-expanded when expanded', async () => {
71+
const button = await renderComponent({expanded: true});
72+
expect(button.getAttribute('aria-expanded')).toBe('true');
73+
});
74+
75+
it('should have aria-expanded set to false when not expanded', async () => {
76+
const button = await renderComponent({expanded: false});
77+
expect(button.getAttribute('aria-expanded')).toBe('false');
78+
});
79+
80+
it('should have aria-controls when expanded', async () => {
81+
const button = await renderComponent({
82+
expanded: true,
83+
ariaControls: 'custom-id',
84+
});
85+
expect(button.getAttribute('aria-controls')).toBe('custom-id');
86+
});
87+
88+
it('should have aria-controls when not expanded', async () => {
89+
const button = await renderComponent({
90+
expanded: false,
91+
ariaControls: 'custom-id',
92+
});
93+
expect(button.getAttribute('aria-controls')).toBe('custom-id');
94+
});
95+
96+
it('should render children before question text', async () => {
97+
const button = await renderComponent({}, html`<span class="icon"></span>`);
98+
const icon = button.querySelector('.icon');
99+
expect(icon).toBeInTheDocument();
100+
expect(icon?.textContent).toBe('→');
101+
});
102+
});

0 commit comments

Comments
 (0)