-
Notifications
You must be signed in to change notification settings - Fork 39
refactor(atomic): Migrate SmartSnippet functional components #6602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
8
commits into
main
Choose a base branch
from
copilot/migrate-smartsnippet-components
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fc53c65
Initial plan
Copilot e71f580
feat(atomic): migrate SmartSnippet functional components to Lit
Copilot 56fa228
Changes before error encountered
Copilot 5fb331d
Revert "Changes before error encountered"
alexprudhomme b4b0a79
cool
alexprudhomme 821af93
separate
alexprudhomme e20e9ec
refactor(atomic): use renderFunctionFixture in smart-snippet-feedback…
Copilot 5f65b03
refactor(atomic): use multiClassMap for button classes in feedback ba…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
294 changes: 294 additions & 0 deletions
294
...ts/common/smart-snippets/atomic-smart-snippet/smart-snippet-functional-components.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,294 @@ | ||
| import type {i18n} from 'i18next'; | ||
| import {html, nothing, render} from 'lit'; | ||
| import {beforeEach, describe, expect, it, vi} from 'vitest'; | ||
| import { | ||
| renderSmartSnippetFooter, | ||
| renderSmartSnippetQuestion, | ||
| renderSmartSnippetTruncatedAnswer, | ||
| renderSmartSnippetWrapper, | ||
| type SmartSnippetFooterProps, | ||
| type SmartSnippetQuestionProps, | ||
| type SmartSnippetTruncatedAnswerProps, | ||
| type SmartSnippetWrapperProps, | ||
| } from './smart-snippet-functional-components'; | ||
|
|
||
| describe('#renderSmartSnippetWrapper', () => { | ||
| let container: HTMLElement; | ||
| let mockI18n: i18n; | ||
|
|
||
| beforeEach(() => { | ||
| container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
|
|
||
| mockI18n = { | ||
| t: vi.fn((key: string) => { | ||
| const translations: Record<string, string> = { | ||
| 'smart-snippet': 'Smart Snippet', | ||
| }; | ||
| return translations[key] || key; | ||
| }), | ||
| } as unknown as i18n; | ||
| }); | ||
|
|
||
| const renderWrapper = ( | ||
| props: Partial<SmartSnippetWrapperProps>, | ||
| children = nothing | ||
| ): HTMLElement => { | ||
| const defaultProps: SmartSnippetWrapperProps = { | ||
| i18n: mockI18n, | ||
| headingLevel: undefined, | ||
| ...props, | ||
| }; | ||
| render( | ||
| html`${renderSmartSnippetWrapper({props: defaultProps})(children)}`, | ||
| container | ||
| ); | ||
| return container.querySelector('aside')!; | ||
| }; | ||
|
|
||
| it('should render an aside element with aria-label', () => { | ||
| const aside = renderWrapper({}); | ||
| expect(aside).toBeInTheDocument(); | ||
| expect(aside.getAttribute('aria-label')).toBe('Smart Snippet'); | ||
| }); | ||
|
|
||
| it('should render an article element with the smart-snippet part', () => { | ||
| renderWrapper({}); | ||
| const article = container.querySelector('article[part="smart-snippet"]'); | ||
| expect(article).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render with background and border classes', () => { | ||
| renderWrapper({}); | ||
| const article = container.querySelector('article'); | ||
| expect(article).toHaveClass('bg-background'); | ||
| expect(article).toHaveClass('border-neutral'); | ||
| expect(article).toHaveClass('text-on-background'); | ||
| expect(article).toHaveClass('rounded-lg'); | ||
| expect(article).toHaveClass('border'); | ||
| }); | ||
|
|
||
| it('should render a heading with sr-only class', () => { | ||
| renderWrapper({}); | ||
| const heading = container.querySelector('.sr-only'); | ||
| expect(heading).toBeInTheDocument(); | ||
| expect(heading?.textContent?.trim()).toBe('Smart Snippet'); | ||
| }); | ||
|
|
||
| it('should render heading with level 0 when headingLevel is undefined', () => { | ||
| renderWrapper({headingLevel: undefined}); | ||
| const heading = container.querySelector('div.sr-only'); | ||
| expect(heading).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render heading with specified level', () => { | ||
| renderWrapper({headingLevel: 2}); | ||
| const heading = container.querySelector('h2.sr-only'); | ||
| expect(heading).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render children inside the article', () => { | ||
| const children = html`<p>Test Content</p>`; | ||
| renderWrapper({}, children); | ||
| const article = container.querySelector('article'); | ||
| expect(article?.textContent).toContain('Test Content'); | ||
| }); | ||
|
|
||
| it('should call i18n.t with smart-snippet key', () => { | ||
| renderWrapper({}); | ||
| expect(mockI18n.t).toHaveBeenCalledWith('smart-snippet'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#renderSmartSnippetQuestion', () => { | ||
| let container: HTMLElement; | ||
|
|
||
| beforeEach(() => { | ||
| container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
| }); | ||
|
|
||
| const renderQuestion = ( | ||
| props: Partial<SmartSnippetQuestionProps> | ||
| ): HTMLElement => { | ||
| const defaultProps: SmartSnippetQuestionProps = { | ||
| question: 'What is a smart snippet?', | ||
| headingLevel: undefined, | ||
| ...props, | ||
| }; | ||
| render( | ||
| html`${renderSmartSnippetQuestion({props: defaultProps})}`, | ||
| container | ||
| ); | ||
| return container.firstElementChild as HTMLElement; | ||
| }; | ||
|
|
||
| it('should render the question text', () => { | ||
| const question = renderQuestion({question: 'What is a smart snippet?'}); | ||
| expect(question.textContent?.trim()).toBe('What is a smart snippet?'); | ||
| }); | ||
|
|
||
| it('should render with text-xl and font-bold classes', () => { | ||
| const question = renderQuestion({}); | ||
| expect(question).toHaveClass('text-xl'); | ||
| expect(question).toHaveClass('font-bold'); | ||
| }); | ||
|
|
||
| it('should render with the question part', () => { | ||
| const question = renderQuestion({}); | ||
| expect(question.getAttribute('part')).toBe('question'); | ||
| }); | ||
|
|
||
| it('should render as a div when headingLevel is undefined', () => { | ||
| const question = renderQuestion({headingLevel: undefined}); | ||
| expect(question.tagName.toLowerCase()).toBe('div'); | ||
| }); | ||
|
|
||
| it('should render as a div when headingLevel is 0', () => { | ||
| const question = renderQuestion({headingLevel: 0}); | ||
| expect(question.tagName.toLowerCase()).toBe('div'); | ||
| }); | ||
|
|
||
| it('should render with incremented heading level', () => { | ||
| const question = renderQuestion({headingLevel: 2}); | ||
| expect(question.tagName.toLowerCase()).toBe('h3'); | ||
| }); | ||
|
|
||
| it('should render as h2 when headingLevel is 1', () => { | ||
| const question = renderQuestion({headingLevel: 1}); | ||
| expect(question.tagName.toLowerCase()).toBe('h2'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#renderSmartSnippetTruncatedAnswer', () => { | ||
| let container: HTMLElement; | ||
|
|
||
| beforeEach(() => { | ||
| container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
| }); | ||
|
|
||
| const renderTruncatedAnswer = ( | ||
| props: Partial<SmartSnippetTruncatedAnswerProps> | ||
| ): HTMLElement => { | ||
| const defaultProps: SmartSnippetTruncatedAnswerProps = { | ||
| answer: '<p>This is the answer</p>', | ||
| style: undefined, | ||
| ...props, | ||
| }; | ||
| render( | ||
| html`${renderSmartSnippetTruncatedAnswer({props: defaultProps})}`, | ||
| container | ||
| ); | ||
| return container.firstElementChild as HTMLElement; | ||
| }; | ||
|
|
||
| it('should render a div with truncated-answer part', () => { | ||
| const answer = renderTruncatedAnswer({}); | ||
| expect(answer).toBeInTheDocument(); | ||
| expect(answer.getAttribute('part')).toBe('truncated-answer'); | ||
| }); | ||
|
|
||
| it('should render atomic-smart-snippet-answer component', () => { | ||
| renderTruncatedAnswer({}); | ||
| const answerElement = container.querySelector( | ||
| 'atomic-smart-snippet-answer' | ||
| ); | ||
| expect(answerElement).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should set htmlContent property on atomic-smart-snippet-answer', () => { | ||
| renderTruncatedAnswer({answer: '<p>Test answer</p>'}); | ||
| const answerElement = container.querySelector( | ||
| 'atomic-smart-snippet-answer' | ||
| // biome-ignore lint/suspicious/noExplicitAny: testing property access on custom element | ||
| ) as any; | ||
| expect(answerElement.htmlContent).toBe('<p>Test answer</p>'); | ||
| }); | ||
|
|
||
| it('should set innerStyle property when style is provided', () => { | ||
| renderTruncatedAnswer({style: 'color: red;'}); | ||
| const answerElement = container.querySelector( | ||
| 'atomic-smart-snippet-answer' | ||
| // biome-ignore lint/suspicious/noExplicitAny: testing property access on custom element | ||
| ) as any; | ||
| expect(answerElement.innerStyle).toBe('color: red;'); | ||
| }); | ||
|
|
||
| it('should set exportparts attribute', () => { | ||
| renderTruncatedAnswer({}); | ||
| const answerElement = container.querySelector( | ||
| 'atomic-smart-snippet-answer' | ||
| ); | ||
| expect(answerElement?.getAttribute('exportparts')).toBe('answer'); | ||
| }); | ||
|
|
||
| it('should set part attribute to body', () => { | ||
| renderTruncatedAnswer({}); | ||
| const answerElement = container.querySelector( | ||
| 'atomic-smart-snippet-answer' | ||
| ); | ||
| expect(answerElement?.getAttribute('part')).toBe('body'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#renderSmartSnippetFooter', () => { | ||
| let container: HTMLElement; | ||
| let mockI18n: i18n; | ||
|
|
||
| beforeEach(() => { | ||
| container = document.createElement('div'); | ||
| document.body.appendChild(container); | ||
|
|
||
| mockI18n = { | ||
| t: vi.fn((key: string) => { | ||
| const translations: Record<string, string> = { | ||
| 'smart-snippet-source': 'Source', | ||
| }; | ||
| return translations[key] || key; | ||
| }), | ||
| } as unknown as i18n; | ||
| }); | ||
|
|
||
| const renderFooter = ( | ||
| props: Partial<SmartSnippetFooterProps>, | ||
| children = nothing | ||
| ): HTMLElement => { | ||
| const defaultProps: SmartSnippetFooterProps = { | ||
| i18n: mockI18n, | ||
| ...props, | ||
| }; | ||
| render( | ||
| html`${renderSmartSnippetFooter({props: defaultProps})(children)}`, | ||
| container | ||
| ); | ||
| return container.querySelector('footer')!; | ||
| }; | ||
|
|
||
| it('should render a footer element', () => { | ||
| const footer = renderFooter({}); | ||
| expect(footer).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('should render with footer part', () => { | ||
| const footer = renderFooter({}); | ||
| expect(footer.getAttribute('part')).toBe('footer'); | ||
| }); | ||
|
|
||
| it('should render with aria-label', () => { | ||
| const footer = renderFooter({}); | ||
| expect(footer.getAttribute('aria-label')).toBe('Source'); | ||
| }); | ||
|
|
||
| it('should call i18n.t with smart-snippet-source key', () => { | ||
| renderFooter({}); | ||
| expect(mockI18n.t).toHaveBeenCalledWith('smart-snippet-source'); | ||
| }); | ||
|
|
||
| it('should render children inside the footer', () => { | ||
| const children = html`<p>Footer Content</p>`; | ||
| renderFooter({}, children); | ||
| const footer = container.querySelector('footer'); | ||
| expect(footer?.textContent).toContain('Footer Content'); | ||
| }); | ||
| }); |
83 changes: 83 additions & 0 deletions
83
...ponents/common/smart-snippets/atomic-smart-snippet/smart-snippet-functional-components.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import type {i18n} from 'i18next'; | ||
| import {html} from 'lit'; | ||
| import {ifDefined} from 'lit/directives/if-defined.js'; | ||
| import type { | ||
| FunctionalComponent, | ||
| FunctionalComponentWithChildren, | ||
| } from '@/src/utils/functional-component-utils'; | ||
| import {renderHeading} from '../../heading'; | ||
|
|
||
| export interface SmartSnippetWrapperProps { | ||
| headingLevel?: number; | ||
| i18n: i18n; | ||
| } | ||
|
|
||
| export const renderSmartSnippetWrapper: FunctionalComponentWithChildren< | ||
| SmartSnippetWrapperProps | ||
| > = | ||
| ({props}) => | ||
| (children) => { | ||
| return html`<aside aria-label=${props.i18n.t('smart-snippet')}> | ||
| ${renderHeading({ | ||
| props: {level: props.headingLevel ?? 0, class: 'sr-only'}, | ||
| })(html`${props.i18n.t('smart-snippet')}`)} | ||
| <article | ||
| class="bg-background border-neutral text-on-background rounded-lg border p-6 pb-4" | ||
| part="smart-snippet" | ||
| > | ||
| ${children} | ||
| </article> | ||
| </aside>`; | ||
| }; | ||
|
|
||
| export interface SmartSnippetQuestionProps { | ||
| headingLevel?: number; | ||
| question: string; | ||
| } | ||
|
|
||
| export const renderSmartSnippetQuestion: FunctionalComponent< | ||
| SmartSnippetQuestionProps | ||
| > = ({props}) => { | ||
| return renderHeading({ | ||
| props: { | ||
| level: props.headingLevel ? props.headingLevel + 1 : 0, | ||
| class: 'text-xl font-bold', | ||
| part: 'question', | ||
| }, | ||
| })(html`${props.question}`); | ||
| }; | ||
|
|
||
| export interface SmartSnippetTruncatedAnswerProps { | ||
| answer: string; | ||
| style?: string; | ||
| } | ||
|
|
||
| export const renderSmartSnippetTruncatedAnswer: FunctionalComponent< | ||
| SmartSnippetTruncatedAnswerProps | ||
| > = ({props}) => { | ||
| return html`<div part="truncated-answer"> | ||
| <atomic-smart-snippet-answer | ||
| exportparts="answer" | ||
| part="body" | ||
| .htmlContent=${props.answer} | ||
| .innerStyle=${ifDefined(props.style)} | ||
| ></atomic-smart-snippet-answer> | ||
| </div>`; | ||
| }; | ||
|
|
||
| export interface SmartSnippetFooterProps { | ||
| i18n: i18n; | ||
| } | ||
|
|
||
| export const renderSmartSnippetFooter: FunctionalComponentWithChildren< | ||
| SmartSnippetFooterProps | ||
| > = | ||
| ({props}) => | ||
| (children) => { | ||
| return html`<footer | ||
| part="footer" | ||
| aria-label=${props.i18n.t('smart-snippet-source')} | ||
| > | ||
| ${children} | ||
| </footer>`; | ||
| }; |
File renamed without changes.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.