-
Notifications
You must be signed in to change notification settings - Fork 2.9k
test(react-tags): add hook regression tests for Tag family #36229
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
base: feat/headless-tag-1-refactor
Are you sure you want to change the base?
Changes from all commits
f1a66a1
e47c1f5
28edd89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { renderHook } from '@testing-library/react-hooks'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { TagGroupContextProvider } from '../../contexts/tagGroupContext'; | ||
| import { useInteractionTag_unstable, useInteractionTagBase_unstable } from './useInteractionTag'; | ||
|
|
||
| const wrap = ( | ||
| contextOverrides: Parameters<typeof TagGroupContextProvider>[0]['value'] = { | ||
| handleTagDismiss: () => ({}), | ||
| size: 'medium', | ||
| }, | ||
| ): React.FC<{ children?: React.ReactNode }> => { | ||
| const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( | ||
| <TagGroupContextProvider value={contextOverrides}>{children}</TagGroupContextProvider> | ||
| ); | ||
| return Wrapper; | ||
| }; | ||
|
|
||
| describe('useInteractionTag_unstable', () => { | ||
| it('should add design-only fields (appearance, shape, size) on top of the base state', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook( | ||
| () => useInteractionTag_unstable({ appearance: 'outline', shape: 'circular', size: 'small' }, ref), | ||
| { | ||
| wrapper: wrap(), | ||
| }, | ||
| ); | ||
|
|
||
| expect(result.current.appearance).toBe('outline'); | ||
| expect(result.current.shape).toBe('circular'); | ||
| expect(result.current.size).toBe('small'); | ||
| }); | ||
|
|
||
| it('should default appearance to filled and shape to rounded', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook(() => useInteractionTag_unstable({}, ref), { wrapper: wrap() }); | ||
|
|
||
| expect(result.current.appearance).toBe('filled'); | ||
| expect(result.current.shape).toBe('rounded'); | ||
| }); | ||
|
|
||
| it('should inherit appearance and size from TagGroupContext when not set on props', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook(() => useInteractionTag_unstable({}, ref), { | ||
| wrapper: wrap({ handleTagDismiss: () => ({}), size: 'extra-small', appearance: 'brand' }), | ||
| }); | ||
|
|
||
| expect(result.current.appearance).toBe('brand'); | ||
| expect(result.current.size).toBe('extra-small'); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useInteractionTagBase_unstable', () => { | ||
| it('should NOT expose design-only fields (appearance/shape/size) on base state', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook(() => useInteractionTagBase_unstable({}, ref), { wrapper: wrap() }); | ||
|
|
||
| expect(result.current).not.toHaveProperty('appearance'); | ||
| expect(result.current).not.toHaveProperty('shape'); | ||
| expect(result.current).not.toHaveProperty('size'); | ||
| }); | ||
|
Comment on lines
+54
to
+61
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this test helpful? |
||
|
|
||
| it('should force disabled when TagGroupContext.disabled is true regardless of props', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook(() => useInteractionTagBase_unstable({ disabled: false }, ref), { | ||
| wrapper: wrap({ handleTagDismiss: () => ({}), size: 'medium', disabled: true }), | ||
| }); | ||
| expect(result.current.disabled).toBe(true); | ||
| }); | ||
|
|
||
| it('should derive selected from props OR context.selectedValues containing the tag value', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
|
|
||
| const propSelected = renderHook(() => useInteractionTagBase_unstable({ selected: true, value: 'a' }, ref), { | ||
| wrapper: wrap(), | ||
| }); | ||
| expect(propSelected.result.current.selected).toBe(true); | ||
|
|
||
| const contextSelected = renderHook(() => useInteractionTagBase_unstable({ value: 'a' }, ref), { | ||
| wrapper: wrap({ handleTagDismiss: () => ({}), size: 'medium', selectedValues: ['a'] }), | ||
| }); | ||
| expect(contextSelected.result.current.selected).toBe(true); | ||
|
|
||
| const notSelected = renderHook(() => useInteractionTagBase_unstable({ value: 'b' }, ref), { | ||
| wrapper: wrap({ handleTagDismiss: () => ({}), size: 'medium', selectedValues: ['a'] }), | ||
| }); | ||
| expect(notSelected.result.current.selected).toBe(false); | ||
| }); | ||
|
|
||
| it('should generate interactionTagPrimaryId for use by aria-labelledby', () => { | ||
| const ref = React.createRef<HTMLDivElement>(); | ||
| const { result } = renderHook(() => useInteractionTagBase_unstable({}, ref), { wrapper: wrap() }); | ||
|
|
||
| expect(result.current.interactionTagPrimaryId).toEqual(expect.stringMatching(/^fui-InteractionTagPrimary-/)); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { renderHook } from '@testing-library/react-hooks'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { InteractionTagContextProvider } from '../../contexts/interactionTagContext'; | ||
| import type { InteractionTagContextValue } from '../../contexts/interactionTagContext'; | ||
| import { useInteractionTagPrimary_unstable, useInteractionTagPrimaryBase_unstable } from './useInteractionTagPrimary'; | ||
|
|
||
| const baseContext: InteractionTagContextValue = { | ||
| appearance: 'filled', | ||
| disabled: false, | ||
| handleTagDismiss: () => ({}), | ||
| interactionTagPrimaryId: 'fui-InteractionTagPrimary-_test_', | ||
| selected: false, | ||
| selectedValues: [], | ||
| shape: 'rounded', | ||
| size: 'medium', | ||
| value: 'test', | ||
| }; | ||
|
|
||
| const wrap = ( | ||
| overrides: Partial<Parameters<typeof InteractionTagContextProvider>[0]['value']> = {}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ): React.FC<{ children?: React.ReactNode }> => { | ||
| const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( | ||
| <InteractionTagContextProvider value={{ ...baseContext, ...overrides }}>{children}</InteractionTagContextProvider> | ||
| ); | ||
| return Wrapper; | ||
| }; | ||
|
|
||
| describe('useInteractionTagPrimary_unstable', () => { | ||
| it('should add design-only fields (appearance, shape, size, avatar*) on top of the base state', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimary_unstable({}, ref), { | ||
| wrapper: wrap({ appearance: 'brand', shape: 'circular', size: 'small' }), | ||
| }); | ||
|
|
||
| expect(result.current.appearance).toBe('brand'); | ||
| expect(result.current.shape).toBe('circular'); | ||
| expect(result.current.size).toBe('small'); | ||
| expect(result.current.avatarShape).toBe('circular'); | ||
| expect(result.current.avatarSize).toBe(20); | ||
| }); | ||
| }); | ||
|
|
||
| describe('useInteractionTagPrimaryBase_unstable', () => { | ||
| it('should render root with the interactionTagPrimaryId from context', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimaryBase_unstable({}, ref), { wrapper: wrap() }); | ||
| expect(result.current.root.id).toBe('fui-InteractionTagPrimary-_test_'); | ||
| }); | ||
|
|
||
| it('should NOT expose design-only fields (appearance/shape/size/avatar*) on base state', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimaryBase_unstable({}, ref), { wrapper: wrap() }); | ||
|
|
||
| expect(result.current).not.toHaveProperty('appearance'); | ||
| expect(result.current).not.toHaveProperty('shape'); | ||
| expect(result.current).not.toHaveProperty('size'); | ||
| expect(result.current).not.toHaveProperty('avatarShape'); | ||
| expect(result.current).not.toHaveProperty('avatarSize'); | ||
| }); | ||
|
Comment on lines
+51
to
+60
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this helpful? |
||
|
|
||
| it('should set aria-pressed when context has handleTagSelect (selectable group)', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimaryBase_unstable({}, ref), { | ||
| wrapper: wrap({ selected: true, handleTagSelect: () => ({}) }), | ||
| }); | ||
| expect(result.current.root['aria-pressed']).toBe(true); | ||
| }); | ||
|
|
||
| it('should NOT set aria-pressed when context has no handleTagSelect', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimaryBase_unstable({}, ref), { | ||
| wrapper: wrap({ selected: true, handleTagSelect: undefined }), | ||
| }); | ||
| expect(result.current.root).not.toHaveProperty('aria-pressed'); | ||
| }); | ||
|
|
||
| it('should default hasSecondaryAction to false', () => { | ||
| const ref = React.createRef<HTMLButtonElement>(); | ||
| const { result } = renderHook(() => useInteractionTagPrimaryBase_unstable({}, ref), { wrapper: wrap() }); | ||
| expect(result.current.hasSecondaryAction).toBe(false); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,106 @@ | ||||||
| import { renderHook } from '@testing-library/react-hooks'; | ||||||
| import * as React from 'react'; | ||||||
|
|
||||||
| import { InteractionTagContextProvider } from '../../contexts/interactionTagContext'; | ||||||
| import type { InteractionTagContextValue } from '../../contexts/interactionTagContext'; | ||||||
| import { | ||||||
| useInteractionTagSecondary_unstable, | ||||||
| useInteractionTagSecondaryBase_unstable, | ||||||
| } from './useInteractionTagSecondary'; | ||||||
|
|
||||||
| const baseContext: InteractionTagContextValue = { | ||||||
| appearance: 'filled', | ||||||
| disabled: false, | ||||||
| handleTagDismiss: () => ({}), | ||||||
| interactionTagPrimaryId: 'fui-InteractionTagPrimary-_test_', | ||||||
| selected: false, | ||||||
| selectedValues: [], | ||||||
| shape: 'rounded', | ||||||
| size: 'medium', | ||||||
| value: 'test', | ||||||
| }; | ||||||
|
|
||||||
| const wrap = ( | ||||||
| overrides: Partial<Parameters<typeof InteractionTagContextProvider>[0]['value']> = {}, | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| ): React.FC<{ children?: React.ReactNode }> => { | ||||||
| const Wrapper: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( | ||||||
| <InteractionTagContextProvider value={{ ...baseContext, ...overrides }}>{children}</InteractionTagContextProvider> | ||||||
| ); | ||||||
| return Wrapper; | ||||||
| }; | ||||||
|
|
||||||
| describe('useInteractionTagSecondary_unstable', () => { | ||||||
| it('should inject DismissRegular as default root children', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondary_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current.root.children).toBeDefined(); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add at least |
||||||
| }); | ||||||
|
|
||||||
| it('should preserve user-provided children instead of the default DismissRegular', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondary_unstable({ children: 'X' }, ref), { | ||||||
| wrapper: wrap(), | ||||||
| }); | ||||||
| expect(result.current.root.children).toBe('X'); | ||||||
| }); | ||||||
|
|
||||||
| it('should inherit appearance/shape/size from context', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondary_unstable({}, ref), { | ||||||
| wrapper: wrap({ appearance: 'outline', shape: 'circular', size: 'small' }), | ||||||
| }); | ||||||
| expect(result.current.appearance).toBe('outline'); | ||||||
| expect(result.current.shape).toBe('circular'); | ||||||
| expect(result.current.size).toBe('small'); | ||||||
| }); | ||||||
| }); | ||||||
|
|
||||||
| describe('useInteractionTagSecondaryBase_unstable', () => { | ||||||
| it('should render root with type="button"', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current.root.type).toBe('button'); | ||||||
| }); | ||||||
|
|
||||||
| it('should NOT inject DismissRegular children by default (icon injection lives in the styled hook)', () => { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current.root).not.toHaveProperty('children'); | ||||||
| }); | ||||||
|
|
||||||
| it('should attach onClick and onKeyDown handlers', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current.root.onClick).toEqual(expect.any(Function)); | ||||||
| expect(result.current.root.onKeyDown).toEqual(expect.any(Function)); | ||||||
| }); | ||||||
|
|
||||||
| it('should build aria-labelledby from interactionTagPrimaryId and own id', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current.root['aria-labelledby']).toEqual( | ||||||
| expect.stringMatching(/^fui-InteractionTagPrimary-_test_ fui-InteractionTagSecondary-/), | ||||||
| ); | ||||||
| }); | ||||||
|
|
||||||
| it('should NOT expose design-only fields (appearance/shape/size)', () => { | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { wrapper: wrap() }); | ||||||
| expect(result.current).not.toHaveProperty('appearance'); | ||||||
| expect(result.current).not.toHaveProperty('shape'); | ||||||
| expect(result.current).not.toHaveProperty('size'); | ||||||
| }); | ||||||
|
Comment on lines
+86
to
+92
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this useful? |
||||||
|
|
||||||
| it('should call handleTagDismiss on Delete/Backspace keyDown via context', () => { | ||||||
| const handleTagDismiss = jest.fn(); | ||||||
| const ref = React.createRef<HTMLButtonElement>(); | ||||||
| const { result } = renderHook(() => useInteractionTagSecondaryBase_unstable({}, ref), { | ||||||
| wrapper: wrap({ handleTagDismiss, value: 'val' }), | ||||||
| }); | ||||||
|
|
||||||
| const event = { key: 'Delete', defaultPrevented: false } as unknown as React.KeyboardEvent<HTMLButtonElement>; | ||||||
| result.current.root.onKeyDown?.(event); | ||||||
|
|
||||||
| expect(handleTagDismiss).toHaveBeenCalledWith(event, { value: 'val' }); | ||||||
| }); | ||||||
| }); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is
TagGroupContextValue