From 518c296608d3c120791ce0acc76c5c0ac1cc2683 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:09:38 +0000 Subject: [PATCH 01/11] Initial plan From 474ab6f66a4a79a7da068f6070e89964989d8525 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 17:23:25 +0000 Subject: [PATCH 02/11] Migrate functional component tests to use renderFunctionFixture - Update button.spec.ts: Remove manual container, use renderFunctionFixture, add page locators - Update radio-button.spec.ts: Remove manual container, use renderFunctionFixture - Update pager-buttons.spec.ts: Remove manual container, use renderFunctionFixture - Update pager-navigation.spec.ts: Remove manual container, use renderFunctionFixture - Update item-list.spec.ts: Remove manual container, use renderFunctionFixture - Update tab-wrapper.spec.ts: Remove manual container, use renderFunctionFixture All tests now follow the correct pattern with async rendering and proper cleanup. Co-authored-by: alexprudhomme <78121423+alexprudhomme@users.noreply.github.com> --- .../src/components/common/button.spec.ts | 140 +++++++------ .../common/item-list/item-list.spec.ts | 31 ++- .../common/pager/pager-buttons.spec.ts | 187 +++++++++--------- .../common/pager/pager-navigation.spec.ts | 36 ++-- .../components/common/radio-button.spec.ts | 134 +++++++------ .../common/tabs/tab-wrapper.spec.ts | 31 ++- 6 files changed, 285 insertions(+), 274 deletions(-) diff --git a/packages/atomic/src/components/common/button.spec.ts b/packages/atomic/src/components/common/button.spec.ts index 3cdc07391e6..07a2d0f4e83 100644 --- a/packages/atomic/src/components/common/button.spec.ts +++ b/packages/atomic/src/components/common/button.spec.ts @@ -1,70 +1,66 @@ -import {html, nothing, render} from 'lit'; -import {fireEvent, within} from 'storybook/test'; -import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {html, nothing} from 'lit'; +import {describe, expect, it, vi} from 'vitest'; +import {page} from 'vitest/browser'; import {createRipple} from '@/src/utils/ripple-utils'; +import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {type ButtonProps, renderButton as button} from './button'; vi.mock('@/src/utils/ripple-utils', {spy: true}); describe('#renderButton', () => { - let container: HTMLElement; - - beforeEach(() => { - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); + const locators = { + get button() { + return page.getByRole('button'); + }, + }; - const renderButton = (props: Partial): HTMLButtonElement => { - render( + const renderButton = async ( + props: Partial + ): Promise => { + return renderFunctionFixture( html`${button({ props: { ...props, style: props.style ?? 'primary', }, - })(nothing)}`, - container + })(nothing)}` ); - return within(container).getByRole('button') as HTMLButtonElement; }; - it('should render a button in the document', () => { - const props = {}; - const button = renderButton(props); - expect(button).toBeInTheDocument(); + it('should render a button in the document', async () => { + await renderButton({}); + await expect.element(locators.button).toBeInTheDocument(); }); - it('should render a button with the correct style', () => { + it('should render a button with the correct style', async () => { const props: Partial = { style: 'outline-error', }; - const button = renderButton(props); + const element = await renderButton(props); + const buttonEl = element.querySelector('button'); - expect(button).toHaveClass('btn-outline-error'); + expect(buttonEl).toHaveClass('btn-outline-error'); }); - it('should render a button with the correct text', () => { + it('should render a button with the correct text', async () => { const props = { text: 'Click me', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.querySelector('span')?.textContent).toBe('Click me'); + expect(element.querySelector('span')?.textContent).toBe('Click me'); }); - it('should wrap the button text with a truncate class', () => { + it('should wrap the button text with a truncate class', async () => { const props = { text: 'Click me', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.querySelector('span')).toHaveClass('truncate'); + expect(element.querySelector('span')).toHaveClass('truncate'); }); it('should handle click event', async () => { @@ -73,110 +69,126 @@ describe('#renderButton', () => { onClick: handleClick, }; - const button = renderButton(props); + const element = await renderButton(props); + const buttonEl = element.querySelector('button')!; - await fireEvent.click(button); + buttonEl.click(); expect(handleClick).toHaveBeenCalled(); }); - it('should apply disabled attribute', () => { + it('should apply disabled attribute', async () => { const props = { disabled: true, }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.hasAttribute('disabled')).toBe(true); + expect(element.querySelector('button')?.hasAttribute('disabled')).toBe( + true + ); }); - it('should apply aria attributes', () => { + it('should apply aria attributes', async () => { const props: Partial = { ariaLabel: 'button', ariaPressed: 'true', }; - const button = renderButton(props); + const element = await renderButton(props); + const buttonEl = element.querySelector('button'); - expect(button.getAttribute('aria-label')).toBe('button'); - expect(button.getAttribute('aria-pressed')).toBe('true'); + expect(buttonEl?.getAttribute('aria-label')).toBe('button'); + expect(buttonEl?.getAttribute('aria-pressed')).toBe('true'); }); - it('should apply custom class', () => { + it('should apply custom class', async () => { const props = { class: 'custom-class', }; - const button = renderButton(props); + const element = await renderButton(props); + const buttonEl = element.querySelector('button'); - expect(button).toHaveClass('custom-class'); - expect(button).toHaveClass('btn-primary'); + expect(buttonEl).toHaveClass('custom-class'); + expect(buttonEl).toHaveClass('btn-primary'); }); - it('should apply part attribute', () => { + it('should apply part attribute', async () => { const props = { part: 'button-part', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('part')).toBe('button-part'); + expect(element.querySelector('button')?.getAttribute('part')).toBe( + 'button-part' + ); }); - it('should apply title attribute', () => { + it('should apply title attribute', async () => { const props = { title: 'Button Title', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('title')).toBe('Button Title'); + expect(element.querySelector('button')?.getAttribute('title')).toBe( + 'Button Title' + ); }); - it('should apply tabindex attribute', () => { + it('should apply tabindex attribute', async () => { const props = { tabIndex: 1, }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('tabindex')).toBe('1'); + expect(element.querySelector('button')?.getAttribute('tabindex')).toBe('1'); }); - it('should apply role attribute', () => { + it('should apply role attribute', async () => { const props: Partial = { role: 'button', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('role')).toBe('button'); + expect(element.querySelector('button')?.getAttribute('role')).toBe( + 'button' + ); }); it('should call onMouseDown when the mousedown event is fired on the button', async () => { const props: Partial = {}; - const button = renderButton(props); - await fireEvent.mouseDown(button); + const element = await renderButton(props); + const buttonEl = element.querySelector('button')!; + buttonEl.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})); expect(createRipple).toHaveBeenCalled(); }); - it('should apply form attribute', () => { + it('should apply form attribute', async () => { const props = { form: 'form-id', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('form')).toBe('form-id'); + expect(element.querySelector('button')?.getAttribute('form')).toBe( + 'form-id' + ); }); - it('should apply type attribute', () => { + it('should apply type attribute', async () => { const props: Partial = { type: 'submit', }; - const button = renderButton(props); + const element = await renderButton(props); - expect(button.getAttribute('type')).toBe('submit'); + expect(element.querySelector('button')?.getAttribute('type')).toBe( + 'submit' + ); }); }); diff --git a/packages/atomic/src/components/common/item-list/item-list.spec.ts b/packages/atomic/src/components/common/item-list/item-list.spec.ts index 6c4f30724cd..5e7bfcc0e8c 100644 --- a/packages/atomic/src/components/common/item-list/item-list.spec.ts +++ b/packages/atomic/src/components/common/item-list/item-list.spec.ts @@ -1,16 +1,14 @@ -import {html, nothing, render, type TemplateResult} from 'lit'; +import {html, nothing, type TemplateResult} from 'lit'; import {describe, expect, it} from 'vitest'; +import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {type ItemListProps, renderItemList} from './item-list'; describe('renderItemList', () => { - const renderItemListWithChildren = ( + const renderItemListWithChildren = async ( props: Partial = {}, children: TemplateResult | typeof nothing = nothing ) => { - const container = document.createElement('div'); - document.body.appendChild(container); - - render( + return renderFunctionFixture( renderItemList({ props: { hasError: false, @@ -20,11 +18,8 @@ describe('renderItemList', () => { templateHasError: false, ...props, }, - })(children), - container + })(children) ); - - return container; }; const testChild = html`
Test Child
`; @@ -42,14 +37,12 @@ describe('renderItemList', () => { description: '#firstRequestExecuted is false and #hasItems is false', props: {firstRequestExecuted: false, hasItems: false}, }, - ])('should render children when $description', ({props}) => { - const container = renderItemListWithChildren(props, testChild); - const child = container.querySelector('.child'); + ])('should render children when $description', async ({props}) => { + const element = await renderItemListWithChildren(props, testChild); + const child = element.querySelector('.child'); expect(child).toBeInTheDocument(); expect(child?.textContent).toBe('Test Child'); - - container.remove(); }); it.each([ @@ -65,12 +58,10 @@ describe('renderItemList', () => { description: '#firstRequestExecuted is true and #hasItems is false', props: {firstRequestExecuted: true, hasItems: false}, }, - ])('should not render children when $description', ({props}) => { - const container = renderItemListWithChildren(props, testChild); - const child = container.querySelector('.child'); + ])('should not render children when $description', async ({props}) => { + const element = await renderItemListWithChildren(props, testChild); + const child = element.querySelector('.child'); expect(child).not.toBeInTheDocument(); - - container.remove(); }); }); diff --git a/packages/atomic/src/components/common/pager/pager-buttons.spec.ts b/packages/atomic/src/components/common/pager/pager-buttons.spec.ts index 1fd4ee236ea..7fafe3fe401 100644 --- a/packages/atomic/src/components/common/pager/pager-buttons.spec.ts +++ b/packages/atomic/src/components/common/pager/pager-buttons.spec.ts @@ -1,6 +1,7 @@ import type {i18n as I18n} from 'i18next'; -import {html, render} from 'lit'; -import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'; +import {html} from 'lit'; +import {beforeAll, describe, expect, test, vi} from 'vitest'; +import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils'; import ArrowLeftIcon from '../../../images/arrow-left-rounded.svg'; import ArrowRightIcon from '../../../images/arrow-right-rounded.svg'; @@ -12,16 +13,15 @@ import { } from './pager-buttons'; describe('pagerButtons', () => { - let container: HTMLElement; let i18n: I18n; - describe('pagerPreviousButton', () => { - beforeEach(async () => { - i18n = await createTestI18n(); - container = document.createElement('div'); - document.body.appendChild(container); + beforeAll(async () => { + i18n = await createTestI18n(); + }); - render( + describe('pagerPreviousButton', () => { + test('should render the button with the correct attributes', async () => { + const element = await renderFunctionFixture( html` ${renderPagerPreviousButton({ props: { @@ -29,34 +29,35 @@ describe('pagerButtons', () => { icon: ArrowLeftIcon, }, })} - `, - container + ` ); - }); - afterEach(() => { - document.body.removeChild(container); - }); - - test('should render the button with the correct attributes', () => { - const button = container.querySelector('button'); + const button = element.querySelector('button'); expect(button).toHaveAttribute('aria-label', 'Previous'); expect(button).toHaveClass('btn-outline-primary'); expect(button).toHaveAttribute('part', 'previous-button'); }); - test('should render the icon with the correct attributes', () => { - const icon = container.querySelector('atomic-icon'); + test('should render the icon with the correct attributes', async () => { + const element = await renderFunctionFixture( + html` + ${renderPagerPreviousButton({ + props: { + i18n, + icon: ArrowLeftIcon, + }, + })} + ` + ); + + const icon = element.querySelector('atomic-icon'); expect(icon).toHaveAttribute('part', 'previous-button-icon'); }); }); describe('pagerNextButton', () => { - beforeEach(async () => { - i18n = await createTestI18n(); - container = document.createElement('div'); - document.body.appendChild(container); - render( + test('should render the button with the correct attributes', async () => { + const element = await renderFunctionFixture( html` ${renderPagerNextButton({ props: { @@ -64,41 +65,35 @@ describe('pagerButtons', () => { icon: ArrowRightIcon, }, })} - `, - container + ` ); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - test('should render the button with the correct attributes', () => { - const button = container.querySelector('button'); + const button = element.querySelector('button'); expect(button).toHaveAttribute('aria-label', 'Next'); expect(button).toHaveClass('btn-outline-primary'); expect(button).toHaveAttribute('part', 'next-button'); }); - test('should render the icon with the correct attributes', () => { - const icon = container.querySelector('atomic-icon'); + test('should render the icon with the correct attributes', async () => { + const element = await renderFunctionFixture( + html` + ${renderPagerNextButton({ + props: { + i18n, + icon: ArrowRightIcon, + }, + })} + ` + ); + + const icon = element.querySelector('atomic-icon'); expect(icon).toHaveClass('w-5'); }); }); describe('pagerPageButton', () => { - beforeEach(async () => { - i18n = await createTestI18n(); - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - }); - - test('should render the button with the correct attributes', () => { - render( + test('should render the button with the correct attributes', async () => { + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -108,19 +103,18 @@ describe('pagerButtons', () => { text: '1', }, })} - `, - container + ` ); - const input = container.querySelector('input'); + const input = element.querySelector('input'); expect(input).toHaveAttribute('aria-label', '1'); expect(input).toHaveAttribute('type', 'radio'); expect(input).toHaveAttribute('name', 'pager'); expect(input).toHaveAttribute('value', '1'); }); - test('should render with the correct attributes when not selected', () => { - render( + test('should render with the correct attributes when not selected', async () => { + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -130,17 +124,16 @@ describe('pagerButtons', () => { text: '1', }, })} - `, - container + ` ); - const input = container.querySelector('input'); + const input = element.querySelector('input'); expect(input).toHaveAttribute('aria-current', 'false'); expect(input).toHaveAttribute('part', 'page-button'); }); - test('should render with the correct attributes when selected', () => { - render( + test('should render with the correct attributes when selected', async () => { + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -150,22 +143,18 @@ describe('pagerButtons', () => { text: '1', }, })} - `, - container + ` ); - const input = container.querySelector('input'); + const input = element.querySelector('input'); expect(input).toHaveAttribute('aria-current', 'page'); expect(input).toHaveAttribute('part', 'page-button active-page-button'); }); }); describe('pagerPageButtons', () => { - beforeEach(async () => { - i18n = await createTestI18n(); - container = document.createElement('div'); - document.body.appendChild(container); - render( + test('should render the list of buttons with the correct attributes', async () => { + const element = await renderFunctionFixture( html` ${renderPageButtons({ props: { @@ -184,37 +173,46 @@ describe('pagerButtons', () => { }, })} `)} - `, - container + ` ); - }); - afterEach(() => { - document.body.removeChild(container); - }); - - test('should render the list of buttons with the correct attributes', () => { - const div = container.querySelector('div'); + const div = element.querySelector('div'); expect(div).toHaveAttribute('role', 'radiogroup'); expect(div).toHaveAttribute('aria-label', 'Pagination'); expect(div).toHaveAttribute('part', 'page-buttons'); }); - test('should render the list of children', () => { - const inputs = container.querySelectorAll('input'); + test('should render the list of children', async () => { + const element = await renderFunctionFixture( + html` + ${renderPageButtons({ + props: { + i18n, + }, + })(html` + ${renderPagerPageButton({ + props: {groupName: 'pager', page: 1, isSelected: true, text: '1'}, + })} + ${renderPagerPageButton({ + props: { + groupName: 'pager', + page: 2, + isSelected: false, + text: '2', + }, + })} + `)} + ` + ); + + const inputs = element.querySelectorAll('input'); expect(inputs).toHaveLength(2); }); }); describe('accessibility', () => { - beforeEach(async () => { - i18n = await createTestI18n(); - container = document.createElement('div'); - document.body.appendChild(container); - }); - - test('should render with aria-roledescription set to link', () => { - render( + test('should render with aria-roledescription set to link', async () => { + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -224,18 +222,17 @@ describe('pagerButtons', () => { text: '1', }, })} - `, - container + ` ); - const input = container.querySelector('input'); + const input = element.querySelector('input'); expect(input).toHaveAttribute('aria-roledescription', 'link'); }); test('should change focus target when input is tab', async () => { const onFocusCallback = vi.fn().mockResolvedValue(undefined); - render( + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -254,11 +251,10 @@ describe('pagerButtons', () => { onFocusCallback, }, })} - `, - container + ` ); - const elements = Array.from(container.querySelectorAll('[type="radio"]')); + const elements = Array.from(element.querySelectorAll('[type="radio"]')); elements[0].dispatchEvent( new KeyboardEvent('keydown', {key: 'Tab', bubbles: true}) ); @@ -275,7 +271,7 @@ describe('pagerButtons', () => { test('should change focus target when input is shift + tab', async () => { const onFocusCallback = vi.fn().mockResolvedValue(undefined); - render( + const element = await renderFunctionFixture( html` ${renderPagerPageButton({ props: { @@ -294,11 +290,10 @@ describe('pagerButtons', () => { onFocusCallback, }, })} - `, - container + ` ); - const elements = Array.from(container.querySelectorAll('[type="radio"]')); + const elements = Array.from(element.querySelectorAll('[type="radio"]')); elements[1].dispatchEvent( new KeyboardEvent('keydown', { key: 'Tab', diff --git a/packages/atomic/src/components/common/pager/pager-navigation.spec.ts b/packages/atomic/src/components/common/pager/pager-navigation.spec.ts index 6339cbcebb6..594cd68dee4 100644 --- a/packages/atomic/src/components/common/pager/pager-navigation.spec.ts +++ b/packages/atomic/src/components/common/pager/pager-navigation.spec.ts @@ -1,11 +1,12 @@ import i18next, {type i18n as I18n} from 'i18next'; -import {html, render} from 'lit'; +import {html} from 'lit'; import {beforeAll, describe, expect, test} from 'vitest'; import enTranslations from '@/dist/atomic/lang/en.json'; +import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture'; import {renderPagerNavigation} from './pager-navigation'; describe('pagerNavigation', () => { - let container: HTMLElement; + let element: HTMLElement; let i18n: I18n; beforeAll(async () => { @@ -18,47 +19,46 @@ describe('pagerNavigation', () => { }, }, }); - container = document.createElement('div'); - document.body.appendChild(container); - render( - html`${renderPagerNavigation({props: {i18n}})(html`children`)}`, - container + element = await renderFunctionFixture( + html`${renderPagerNavigation({props: {i18n}})(html`children`)}` ); }); test('should render a