diff --git a/apps/vr-tests-react-components/src/stories/AvatarGroup.stories.tsx b/apps/vr-tests-react-components/src/stories/AvatarGroup.stories.tsx new file mode 100644 index 00000000000000..d6d7ef2145aabe --- /dev/null +++ b/apps/vr-tests-react-components/src/stories/AvatarGroup.stories.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { storiesOf } from '@storybook/react'; +import Screener from 'screener-storybook/src/screener'; +import { AvatarGroup, AvatarGroupItem, AvatarGroupProps } from '@fluentui/react-avatar'; +import { TestWrapperDecorator } from '../utilities/TestWrapperDecorator'; + +const names = [ + 'Katri Athokas', + 'Elvia Atkins', + 'Mauricio August', + 'Colin Ballinger', + 'Lydia Bauer', + 'Amanda Brady', + 'Henry Brill', + 'Celeste Burton', + 'Robin Counts', + 'Tim Deboer', + 'Cameron Evans', + 'Isaac Fielder', + 'Cecil Folk', + 'Miguel Garcia', + 'Wanda Howard', + 'Mona Kane', + 'Kat Larsson', + 'Ashley McCarthy', + 'Johnie McConnell', + 'Allan Munger', + 'Erik Nason', + 'Kristin Patterson', + 'Daisy Phillips', + 'Carole Poland', + 'Carlos Slattery', + 'Robert Tolbert', + 'Kevin Sturgis', + 'Charlotte Waltson', + 'Elliot Woodward', +]; + +const sizes = [16, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 96, 120, 128]; + +const AvatarGroupList: React.FC = props => { + return ( +
+ {sizes.map(size => ( + + {names.map(name => ( + + ))} + + ))} +
+ ); +}; + +// Non-interactive stories +storiesOf('AvatarGroup Converged', module) + .addDecorator(TestWrapperDecorator) + .addDecorator(story => ( + {story()} + )) + .addStory('basic', () => , { + includeHighContrast: true, + includeDarkMode: true, + }) + .addStory('layoutPie', () => , { + includeHighContrast: true, + includeDarkMode: true, + }) + .addStory('layoutStack', () => , { + includeHighContrast: true, + includeDarkMode: true, + }) + .addStory('overflowIndicator', () => ); + +// Interactive stories +storiesOf('AvatarGroup Converged', module) + .addDecorator(TestWrapperDecorator) + .addDecorator(story => ( + + {story()} + + )) + .addStory( + 'overflowContent', + () => ( + + {names.map(name => ( + + ))} + + ), + { + includeHighContrast: true, + includeDarkMode: true, + }, + ); diff --git a/change/@fluentui-react-avatar-0ebe3e03-9d8f-48f7-a4c0-2a95b7014e2e.json b/change/@fluentui-react-avatar-0ebe3e03-9d8f-48f7-a4c0-2a95b7014e2e.json new file mode 100644 index 00000000000000..3ce8f528ae52b8 --- /dev/null +++ b/change/@fluentui-react-avatar-0ebe3e03-9d8f-48f7-a4c0-2a95b7014e2e.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: Adding unit tests for AvatarGroup. ", + "packageName": "@fluentui/react-avatar", + "email": "esteban.230@hotmail.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.test.tsx b/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.test.tsx index 50ff191fa4ac91..afd352337a0650 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.test.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroup/AvatarGroup.test.tsx @@ -2,24 +2,176 @@ import * as React from 'react'; import { AvatarGroup } from './AvatarGroup'; import { AvatarGroupItem } from '../AvatarGroupItem'; import { isConformant } from '../../common/isConformant'; -import { render } from '@testing-library/react'; +import { render, RenderResult, screen, within } from '@testing-library/react'; +import { avatarGroupClassNames } from './useAvatarGroupStyles'; + +// testing-library's queryByRole function doesn't look inside portals +function queryByRoleList(result: RenderResult) { + const lists = result.baseElement.querySelectorAll('*[role="list"]'); + if (!lists?.length) { + return null; + } else { + expect(lists.length).toBe(1); + return lists.item(0) as HTMLElement; + } +} + +const getOverflowContentElement = (result: RenderResult) => { + // overflowButton needs to be clicked otherwise overflowContent won't be rendered. + result.queryByRole('button')?.click(); + return queryByRoleList(result)!; +}; describe('AvatarGroup', () => { - // TODO: Remove component-has-static-classnames-object from disabled tests. isConformant({ Component: AvatarGroup, displayName: 'AvatarGroup', disabledTests: [ 'component-has-static-classname', 'component-has-static-classname-exported', - 'component-has-static-classnames-object', + 'make-styles-overrides-win', ], + testOptions: { + 'has-static-classnames': [ + { + props: {}, + expectedClassNames: { + root: avatarGroupClassNames.root, + overflowButton: avatarGroupClassNames.overflowButton, + overflowContent: avatarGroupClassNames.overflowContent, + }, + getPortalElement: getOverflowContentElement, + }, + ], + }, + requiredProps: { + children: [ + , + , + , + , + , + , + , + , + , + ], + }, + }); + + it('renders an overflow indicator when AvatarGroupItems overflow', () => { + render( + + + + + + + + + + + , + ); + + expect(screen.getByText('+5')).toBeTruthy(); + }); + + it('renders an icon overflow indicator when size is less than 24', () => { + render( + + + + + + + + + + + , + ); + + expect(screen.getByRole('button').textContent).toBe(''); + }); + + it('ignores maxAvatars when using pie layout', () => { + render( + + + + + + + + + + + , + ); + + // maxAvatars is 3 when the layout is pie, but it also renders a transparent button + // so it needs to check for 3 children + 1 button + expect(screen.getByRole('group').children.length).toBe(4); }); - // TODO add more tests here, and create visual regression tests in /apps/vr-tests + it('renders the avatars in the correct order when using a stack or spread layout', () => { + render( + + + + + + + + + + + , + ); - it('renders a default state', () => { - const result = render( + const avatarChildren = screen.getByRole('group').children; + + expect(avatarChildren.item(0)?.textContent).toBe('AM'); + expect(avatarChildren.item(1)?.textContent).toBe('DP'); + expect(avatarChildren.item(2)?.textContent).toBe('RT'); + expect(avatarChildren.item(3)?.textContent).toBe('KS'); + }); + + it('renders all AvatarGroupItems inside the overflowContent when using a pie layout', () => { + const { baseElement } = render( + + + + + + + + + + + , + ); + + const button = screen.getByRole('button'); + button.click(); + // overflowContent is rendered in a portal, so using baseElement let's you access its content + const avatarGroupItems = within(baseElement as HTMLElement).getAllByRole('listitem'); + const avatarGroupItemAvatars = avatarGroupItems.map(item => within(item).getByRole('img')); + + expect(avatarGroupItemAvatars[0].textContent).toBe('KA'); + expect(avatarGroupItemAvatars[1].textContent).toBe('EA'); + expect(avatarGroupItemAvatars[2].textContent).toBe('CE'); + expect(avatarGroupItemAvatars[3].textContent).toBe('WH'); + expect(avatarGroupItemAvatars[4].textContent).toBe('MK'); + expect(avatarGroupItemAvatars[5].textContent).toBe('AM'); + expect(avatarGroupItemAvatars[6].textContent).toBe('DP'); + expect(avatarGroupItemAvatars[7].textContent).toBe('RT'); + expect(avatarGroupItemAvatars[8].textContent).toBe('KS'); + }); + + it('handles maxAvatars', () => { + render( @@ -32,6 +184,25 @@ describe('AvatarGroup', () => { , ); - expect(result.container).toMatchSnapshot(); + + expect(screen.getByRole('group').children.length).toBe(5); + }); + + it('handles custom maxAvatars', () => { + render( + + + + + + + + + + + , + ); + + expect(screen.getByRole('group').children.length).toBe(8); }); }); diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/__snapshots__/AvatarGroup.test.tsx.snap b/packages/react-components/react-avatar/src/components/AvatarGroup/__snapshots__/AvatarGroup.test.tsx.snap deleted file mode 100644 index bfa93f39543024..00000000000000 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/__snapshots__/AvatarGroup.test.tsx.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AvatarGroup renders a default state 1`] = ` -
-
-
- - - AM - - -
-
- - - DP - - -
-
- - - RT - - -
-
- - - KS - - -
- -
-
-`; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.tsx b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.tsx index 048a3eeffd8ab0..cf87dc41e70776 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroup.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { AvatarGroupItem } from '../AvatarGroupItem/AvatarGroupItem'; import { getNativeElementProps, resolveShorthand } from '@fluentui/react-utilities'; import { MoreHorizontalRegular } from '@fluentui/react-icons'; import { PopoverSurface } from '@fluentui/react-popover'; @@ -19,14 +18,6 @@ export const useAvatarGroup_unstable = (props: AvatarGroupProps, ref: React.Ref< const { overflowIndicator = size < 24 ? 'icon' : 'count' } = props; const childrenArray = React.Children.toArray(children); - if ( - process.env.NODE_ENV !== 'production' && - childrenArray.find(child => React.isValidElement(child) && child.type !== AvatarGroupItem) - ) { - // eslint-disable-next-line no-console - console.warn("AvatarGroup's children must be of type AvatarGroupItems."); - } - let rootChildren = childrenArray; let overflowChildren; let overflowButtonChildren; diff --git a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroupStyles.ts b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroupStyles.ts index 9b33bd69d5bf6b..ab5e115998e290 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroupStyles.ts +++ b/packages/react-components/react-avatar/src/components/AvatarGroup/useAvatarGroupStyles.ts @@ -22,6 +22,7 @@ const useStyles = makeStyles({ }, pie: { clipPath: 'circle(50%)', + backgroundColor: tokens.colorTransparentStroke, '@media (forced-colors: active)': { backgroundColor: 'CanvasText', }, diff --git a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx index 35216e231fbc27..2298b41cba76fa 100644 --- a/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx +++ b/packages/react-components/react-avatar/src/components/AvatarGroupItem/AvatarGroupItem.test.tsx @@ -14,7 +14,11 @@ describe('AvatarGroupItem', () => { isConformant({ Component: AvatarGroupItem, displayName: 'AvatarGroupItem', - disabledTests: ['component-has-static-classname', 'component-has-static-classname-exported'], + disabledTests: [ + 'component-has-static-classname', + 'component-has-static-classname-exported', + 'make-styles-overrides-win', + ], primarySlot: 'avatar', renderOptions: { wrapper: ContextWrapper,