11/**
22 * Component tests for RelationshipTypeFilter
3+ * Tests the categorized accordion + chip UI with preset buttons
34 */
45
56import { RelationType } from '@bibgraph/types' ;
67import { MantineProvider } from '@mantine/core' ;
7- import { cleanup , render , screen } from '@testing-library/react' ;
8+ import { cleanup , render , screen } from '@testing-library/react' ;
89import { userEvent } from '@testing-library/user-event' ;
9- import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
10+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
1011
1112import { RelationshipTypeFilter } from './RelationshipTypeFilter' ;
1213
@@ -49,115 +50,110 @@ describe('RelationshipTypeFilter', () => {
4950 expect ( screen . getByText ( 'Filter by Relationship Type' ) ) . toBeInTheDocument ( ) ;
5051 } ) ;
5152
52- it ( 'should render checkboxes for all relationship types ' , ( ) => {
53+ it ( 'should render preset buttons ' , ( ) => {
5354 renderComponent ( ) ;
5455
55- // Verify all unique RelationType values have checkboxes
56- const allTypes = getUniqueTypes ( ) ;
57- allTypes . forEach ( ( type ) => {
58- const checkbox = screen . getByTestId ( `filter-checkbox-${ type } ` ) ;
59- expect ( checkbox ) . toBeInTheDocument ( ) ;
60- } ) ;
56+ expect ( screen . getByTestId ( 'preset-all' ) ) . toBeInTheDocument ( ) ;
57+ expect ( screen . getByTestId ( 'preset-core' ) ) . toBeInTheDocument ( ) ;
58+ expect ( screen . getByTestId ( 'preset-citations' ) ) . toBeInTheDocument ( ) ;
6159 } ) ;
6260
63- it ( 'should show all checkboxes as checked when selectedTypes is empty' , ( ) => {
64- renderComponent ( [ ] ) ;
61+ it ( 'should call onChange with empty array when All preset is clicked' , async ( ) => {
62+ const user = userEvent . setup ( ) ;
63+ const selectedTypes = [ RelationType . AUTHORSHIP ] ;
64+ renderComponent ( selectedTypes ) ;
6565
66- const allTypes = getUniqueTypes ( ) ;
67- allTypes . forEach ( ( type ) => {
68- // Mantine passes data-testid to the wrapper, find input inside
69- const checkboxWrapper = screen . getByTestId ( `filter-checkbox-${ type } ` ) ;
70- const input = checkboxWrapper . closest ( '.mantine-Checkbox-root' ) ?. querySelector ( 'input[type="checkbox"]' ) as HTMLInputElement ;
71- expect ( input ) . toBeChecked ( ) ;
72- } ) ;
73- } ) ;
66+ const allPreset = screen . getByTestId ( 'preset-all' ) ;
67+ await user . click ( allPreset ) ;
7468
75- it ( 'should only check selected types when selectedTypes is not empty' , ( ) => {
76- const selectedTypes = [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ;
77- renderComponent ( selectedTypes ) ;
69+ expect ( mockOnChange ) . toHaveBeenCalledWith ( [ ] ) ;
70+ } ) ;
7871
79- const authorshipCheckbox = screen . getByTestId ( `filter-checkbox- ${ RelationType . AUTHORSHIP } ` ) ;
80- const referenceCheckbox = screen . getByTestId ( `filter-checkbox- ${ RelationType . REFERENCE } ` ) ;
81- const publicationCheckbox = screen . getByTestId ( `filter-checkbox- ${ RelationType . PUBLICATION } ` ) ;
72+ it ( 'should call onChange with core types when Core Only preset is clicked' , async ( ) => {
73+ const user = userEvent . setup ( ) ;
74+ renderComponent ( [ ] ) ;
8275
83- const authorshipInput = authorshipCheckbox . closest ( '.mantine-Checkbox-root' ) ?. querySelector ( 'input[type="checkbox"]' ) as HTMLInputElement ;
84- const referenceInput = referenceCheckbox . closest ( '.mantine-Checkbox-root' ) ?. querySelector ( 'input[type="checkbox"]' ) as HTMLInputElement ;
85- const publicationInput = publicationCheckbox . closest ( '.mantine-Checkbox-root' ) ?. querySelector ( 'input[type="checkbox"]' ) as HTMLInputElement ;
76+ const corePreset = screen . getByTestId ( 'preset-core' ) ;
77+ await user . click ( corePreset ) ;
8678
87- expect ( authorshipInput ) . toBeChecked ( ) ;
88- expect ( referenceInput ) . toBeChecked ( ) ;
89- expect ( publicationInput ) . not . toBeChecked ( ) ;
79+ expect ( mockOnChange ) . toHaveBeenCalledWith ( [
80+ RelationType . AUTHORSHIP ,
81+ RelationType . AFFILIATION ,
82+ RelationType . PUBLICATION ,
83+ RelationType . REFERENCE ,
84+ RelationType . TOPIC ,
85+ ] ) ;
9086 } ) ;
9187
92- it ( 'should call onChange when checkbox is toggled ' , async ( ) => {
88+ it ( 'should call onChange with REFERENCE when Citations preset is clicked ' , async ( ) => {
9389 const user = userEvent . setup ( ) ;
9490 renderComponent ( [ ] ) ;
9591
96- const authorshipCheckbox = screen . getByTestId ( `filter-checkbox- ${ RelationType . AUTHORSHIP } ` ) ;
97- await user . click ( authorshipCheckbox ) ;
92+ const citationsPreset = screen . getByTestId ( 'preset-citations' ) ;
93+ await user . click ( citationsPreset ) ;
9894
99- // When empty array (all selected), clicking should select only that one type
100- expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . AUTHORSHIP ] ) ;
95+ expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . REFERENCE ] ) ;
10196 } ) ;
10297
103- it ( 'should add type when checkbox is clicked on unselected type' , async ( ) => {
104- const user = userEvent . setup ( ) ;
105- const selectedTypes = [ RelationType . AUTHORSHIP ] ;
106- renderComponent ( selectedTypes ) ;
98+ it ( 'should render chips for relationship types within accordion' , ( ) => {
99+ renderComponent ( ) ;
107100
108- const referenceCheckbox = screen . getByTestId ( `filter-checkbox-${ RelationType . REFERENCE } ` ) ;
109- await user . click ( referenceCheckbox ) ;
101+ // Core category should be open by default
102+ expect ( screen . getByTestId ( `filter-chip-${ RelationType . AUTHORSHIP } ` ) ) . toBeInTheDocument ( ) ;
103+ expect ( screen . getByTestId ( `filter-chip-${ RelationType . REFERENCE } ` ) ) . toBeInTheDocument ( ) ;
104+ } ) ;
110105
111- expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ) ;
106+ it ( 'should render chips for core category types when empty selection (all shown)' , ( ) => {
107+ renderComponent ( [ ] ) ;
108+
109+ // Chips should be rendered in core category (open by default)
110+ expect ( screen . getByTestId ( `filter-chip-${ RelationType . AUTHORSHIP } ` ) ) . toBeInTheDocument ( ) ;
111+ expect ( screen . getByTestId ( `filter-chip-${ RelationType . REFERENCE } ` ) ) . toBeInTheDocument ( ) ;
112112 } ) ;
113113
114- it ( 'should remove type when checkbox is clicked on selected type' , async ( ) => {
115- const user = userEvent . setup ( ) ;
114+ it ( 'should render chips in core category for specific selection' , ( ) => {
116115 const selectedTypes = [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ;
117116 renderComponent ( selectedTypes ) ;
118117
119- const authorshipCheckbox = screen . getByTestId ( `filter-checkbox- ${ RelationType . AUTHORSHIP } ` ) ;
120- await user . click ( authorshipCheckbox ) ;
121-
122- expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . REFERENCE ] ) ;
118+ // All core category chips should be present
119+ expect ( screen . getByTestId ( `filter-chip- ${ RelationType . AUTHORSHIP } ` ) ) . toBeInTheDocument ( ) ;
120+ expect ( screen . getByTestId ( `filter-chip- ${ RelationType . REFERENCE } ` ) ) . toBeInTheDocument ( ) ;
121+ expect ( screen . getByTestId ( `filter-chip- ${ RelationType . PUBLICATION } ` ) ) . toBeInTheDocument ( ) ;
123122 } ) ;
124123
125- it ( 'should clear all selections when Clear All is clicked ' , async ( ) => {
124+ it ( 'should call onChange when chip is toggled from "all" state ' , async ( ) => {
126125 const user = userEvent . setup ( ) ;
127- const selectedTypes = [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ;
128- renderComponent ( selectedTypes ) ;
126+ renderComponent ( [ ] ) ;
129127
130- const clearButton = screen . getByTestId ( 'clear-all-button' ) ;
131- await user . click ( clearButton ) ;
128+ const authorshipChip = screen . getByTestId ( `filter-chip- ${ RelationType . AUTHORSHIP } ` ) ;
129+ await user . click ( authorshipChip ) ;
132130
133- expect ( mockOnChange ) . toHaveBeenCalledWith ( [ ] ) ;
131+ // When empty array (all selected), clicking should exclude that type
132+ const allTypes = getUniqueTypes ( ) ;
133+ const expectedTypes = allTypes . filter ( t => t !== RelationType . AUTHORSHIP ) ;
134+ expect ( mockOnChange ) . toHaveBeenCalledWith ( expectedTypes ) ;
134135 } ) ;
135136
136- it ( 'should select all types when Select All is clicked' , async ( ) => {
137+ it ( 'should add type when chip is clicked on unselected type ' , async ( ) => {
137138 const user = userEvent . setup ( ) ;
138139 const selectedTypes = [ RelationType . AUTHORSHIP ] ;
139140 renderComponent ( selectedTypes ) ;
140141
141- const selectAllButton = screen . getByTestId ( 'select-all-button' ) ;
142- await user . click ( selectAllButton ) ;
142+ const referenceChip = screen . getByTestId ( `filter-chip- ${ RelationType . REFERENCE } ` ) ;
143+ await user . click ( referenceChip ) ;
143144
144- const allTypes = getUniqueTypes ( ) ;
145- expect ( mockOnChange ) . toHaveBeenCalledWith ( allTypes ) ;
145+ expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ) ;
146146 } ) ;
147147
148- it ( 'should disable Clear All button when no types selected' , ( ) => {
149- renderComponent ( [ ] ) ;
150-
151- const clearButton = screen . getByTestId ( 'clear-all-button' ) ;
152- expect ( clearButton ) . toBeDisabled ( ) ;
153- } ) ;
148+ it ( 'should remove type when chip is clicked on selected type' , async ( ) => {
149+ const user = userEvent . setup ( ) ;
150+ const selectedTypes = [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ;
151+ renderComponent ( selectedTypes ) ;
154152
155- it ( 'should disable Select All button when all types selected' , ( ) => {
156- const allTypes = getUniqueTypes ( ) ;
157- renderComponent ( allTypes ) ;
153+ const authorshipChip = screen . getByTestId ( `filter-chip-${ RelationType . AUTHORSHIP } ` ) ;
154+ await user . click ( authorshipChip ) ;
158155
159- const selectAllButton = screen . getByTestId ( 'select-all-button' ) ;
160- expect ( selectAllButton ) . toBeDisabled ( ) ;
156+ expect ( mockOnChange ) . toHaveBeenCalledWith ( [ RelationType . REFERENCE ] ) ;
161157 } ) ;
162158
163159 it ( 'should use custom title when provided' , ( ) => {
@@ -173,4 +169,35 @@ describe('RelationshipTypeFilter', () => {
173169
174170 expect ( screen . getByText ( 'Custom Filter Title' ) ) . toBeInTheDocument ( ) ;
175171 } ) ;
172+
173+ it ( 'should show selection count badge when types are selected' , ( ) => {
174+ const selectedTypes = [ RelationType . AUTHORSHIP , RelationType . REFERENCE ] ;
175+ renderComponent ( selectedTypes ) ;
176+
177+ expect ( screen . getByText ( '2 selected' ) ) . toBeInTheDocument ( ) ;
178+ } ) ;
179+
180+ it ( 'should not show selection count when showing all (empty array)' , ( ) => {
181+ renderComponent ( [ ] ) ;
182+
183+ expect ( screen . queryByText ( / \d + s e l e c t e d / ) ) . not . toBeInTheDocument ( ) ;
184+ } ) ;
185+
186+ it ( 'should highlight active preset button via class when All is active' , ( ) => {
187+ renderComponent ( [ ] ) ;
188+
189+ // When selectedTypes is empty, "All" preset should be active
190+ // Just verify the button exists and is clickable
191+ const allPreset = screen . getByTestId ( 'preset-all' ) ;
192+ expect ( allPreset ) . toBeInTheDocument ( ) ;
193+ } ) ;
194+
195+ it ( 'should render category toggle buttons' , ( ) => {
196+ renderComponent ( ) ;
197+
198+ // Core category should be open by default - look for its toggle
199+ const coreToggle = screen . getByTestId ( 'category-toggle-core' ) ;
200+ expect ( coreToggle ) . toBeInTheDocument ( ) ;
201+ expect ( coreToggle ) . toHaveTextContent ( 'Deselect All' ) ;
202+ } ) ;
176203} ) ;
0 commit comments