@pie-lib/config-ui 12.0.0-beta.5 → 12.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.json +8 -1653
  2. package/CHANGELOG.md +345 -4
  3. package/LICENSE.md +5 -0
  4. package/NEXT.CHANGELOG.json +1 -0
  5. package/lib/alert-dialog.js +40 -10
  6. package/lib/alert-dialog.js.map +1 -1
  7. package/lib/checkbox.js +58 -48
  8. package/lib/checkbox.js.map +1 -1
  9. package/lib/choice-configuration/feedback-menu.js +24 -26
  10. package/lib/choice-configuration/feedback-menu.js.map +1 -1
  11. package/lib/choice-configuration/index.js +182 -185
  12. package/lib/choice-configuration/index.js.map +1 -1
  13. package/lib/choice-utils.js +5 -7
  14. package/lib/choice-utils.js.map +1 -1
  15. package/lib/feedback-config/feedback-selector.js +69 -73
  16. package/lib/feedback-config/feedback-selector.js.map +1 -1
  17. package/lib/feedback-config/group.js +22 -25
  18. package/lib/feedback-config/group.js.map +1 -1
  19. package/lib/feedback-config/index.js +41 -44
  20. package/lib/feedback-config/index.js.map +1 -1
  21. package/lib/form-section.js +31 -25
  22. package/lib/form-section.js.map +1 -1
  23. package/lib/help.js +37 -47
  24. package/lib/help.js.map +1 -1
  25. package/lib/index.js +1 -2
  26. package/lib/index.js.map +1 -1
  27. package/lib/input.js +12 -17
  28. package/lib/input.js.map +1 -1
  29. package/lib/inputs.js +58 -67
  30. package/lib/inputs.js.map +1 -1
  31. package/lib/langs.js +56 -70
  32. package/lib/langs.js.map +1 -1
  33. package/lib/layout/config-layout.js +78 -47
  34. package/lib/layout/config-layout.js.map +1 -1
  35. package/lib/layout/index.js.map +1 -1
  36. package/lib/layout/layout-contents.js +58 -60
  37. package/lib/layout/layout-contents.js.map +1 -1
  38. package/lib/layout/settings-box.js +25 -33
  39. package/lib/layout/settings-box.js.map +1 -1
  40. package/lib/mui-box/index.js +41 -50
  41. package/lib/mui-box/index.js.map +1 -1
  42. package/lib/number-text-field-custom.js +151 -89
  43. package/lib/number-text-field-custom.js.map +1 -1
  44. package/lib/number-text-field.js +74 -63
  45. package/lib/number-text-field.js.map +1 -1
  46. package/lib/radio-with-label.js +30 -16
  47. package/lib/radio-with-label.js.map +1 -1
  48. package/lib/settings/display-size.js +16 -20
  49. package/lib/settings/display-size.js.map +1 -1
  50. package/lib/settings/index.js +13 -19
  51. package/lib/settings/index.js.map +1 -1
  52. package/lib/settings/panel.js +140 -141
  53. package/lib/settings/panel.js.map +1 -1
  54. package/lib/settings/settings-radio-label.js +29 -16
  55. package/lib/settings/settings-radio-label.js.map +1 -1
  56. package/lib/settings/toggle.js +39 -25
  57. package/lib/settings/toggle.js.map +1 -1
  58. package/lib/tabs/index.js +18 -30
  59. package/lib/tabs/index.js.map +1 -1
  60. package/lib/tags-input/index.js +49 -61
  61. package/lib/tags-input/index.js.map +1 -1
  62. package/lib/two-choice.js +33 -43
  63. package/lib/two-choice.js.map +1 -1
  64. package/lib/with-stateful-model.js +8 -12
  65. package/lib/with-stateful-model.js.map +1 -1
  66. package/package.json +22 -11
  67. package/src/__tests__/alert-dialog.test.jsx +283 -0
  68. package/src/__tests__/checkbox.test.jsx +249 -0
  69. package/src/__tests__/choice-utils.test.js +12 -0
  70. package/src/__tests__/form-section.test.jsx +334 -0
  71. package/src/__tests__/help.test.jsx +184 -0
  72. package/src/__tests__/input.test.jsx +192 -0
  73. package/src/__tests__/langs.test.jsx +457 -0
  74. package/src/__tests__/number-text-field-custom.test.jsx +438 -0
  75. package/src/__tests__/number-text-field.test.jsx +341 -0
  76. package/src/__tests__/radio-with-label.test.jsx +259 -0
  77. package/src/__tests__/settings-panel.test.js +187 -0
  78. package/src/__tests__/settings.test.jsx +515 -0
  79. package/src/__tests__/tabs.test.jsx +193 -0
  80. package/src/__tests__/two-choice.test.js +110 -0
  81. package/src/__tests__/with-stateful-model.test.jsx +145 -0
  82. package/src/alert-dialog.jsx +30 -8
  83. package/src/checkbox.jsx +43 -37
  84. package/src/choice-configuration/__tests__/feedback-menu.test.jsx +163 -0
  85. package/src/choice-configuration/__tests__/index.test.jsx +234 -0
  86. package/src/choice-configuration/feedback-menu.jsx +6 -6
  87. package/src/choice-configuration/index.jsx +208 -192
  88. package/src/feedback-config/__tests__/feedback-config.test.jsx +141 -0
  89. package/src/feedback-config/__tests__/feedback-selector.test.jsx +107 -0
  90. package/src/feedback-config/feedback-selector.jsx +52 -53
  91. package/src/feedback-config/group.jsx +21 -22
  92. package/src/feedback-config/index.jsx +27 -29
  93. package/src/form-section.jsx +26 -18
  94. package/src/help.jsx +20 -28
  95. package/src/input.jsx +1 -1
  96. package/src/inputs.jsx +35 -44
  97. package/src/langs.jsx +41 -46
  98. package/src/layout/__tests__/config.layout.test.jsx +59 -0
  99. package/src/layout/__tests__/layout-content.test.jsx +3 -0
  100. package/src/layout/config-layout.jsx +53 -23
  101. package/src/layout/layout-contents.jsx +38 -40
  102. package/src/layout/settings-box.jsx +16 -19
  103. package/src/mui-box/index.jsx +35 -43
  104. package/src/number-text-field-custom.jsx +117 -65
  105. package/src/number-text-field.jsx +51 -34
  106. package/src/radio-with-label.jsx +26 -10
  107. package/src/settings/display-size.jsx +12 -11
  108. package/src/settings/index.js +2 -1
  109. package/src/settings/panel.jsx +101 -92
  110. package/src/settings/settings-radio-label.jsx +26 -10
  111. package/src/settings/toggle.jsx +37 -18
  112. package/src/tabs/index.jsx +8 -8
  113. package/src/tags-input/__tests__/index.test.jsx +113 -0
  114. package/src/tags-input/index.jsx +35 -38
  115. package/src/two-choice.jsx +15 -19
  116. package/README.md +0 -12
@@ -0,0 +1,192 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import Input from '../input';
5
+
6
+ describe('Input Component', () => {
7
+ const onChange = jest.fn();
8
+
9
+ beforeEach(() => {
10
+ onChange.mockClear();
11
+ });
12
+
13
+ describe('Rendering', () => {
14
+ it('should render input without label', () => {
15
+ const { container } = render(
16
+ <Input type="text" value="test" onChange={onChange} />,
17
+ );
18
+
19
+ const input = container.querySelector('input');
20
+ expect(input).toBeInTheDocument();
21
+ expect(input.value).toBe('test');
22
+ });
23
+
24
+ it('should render input with label', () => {
25
+ render(
26
+ <Input type="text" label="Email" value="test@example.com" onChange={onChange} />,
27
+ );
28
+
29
+ expect(screen.getByText('Email')).toBeInTheDocument();
30
+ const input = screen.getByDisplayValue('test@example.com');
31
+ expect(input).toBeInTheDocument();
32
+ });
33
+
34
+ it('should render input with different types', () => {
35
+ const { container: numberContainer } = render(
36
+ <Input type="number" value={123} onChange={onChange} />,
37
+ );
38
+ expect(numberContainer.querySelector('input[type="number"]')).toBeInTheDocument();
39
+
40
+ const { container: emailContainer } = render(
41
+ <Input type="email" value="test@example.com" onChange={onChange} />,
42
+ );
43
+ expect(emailContainer.querySelector('input[type="email"]')).toBeInTheDocument();
44
+
45
+ const { container: passwordContainer } = render(
46
+ <Input type="password" value="secret" onChange={onChange} />,
47
+ );
48
+ expect(passwordContainer.querySelector('input[type="password"]')).toBeInTheDocument();
49
+ });
50
+ });
51
+
52
+ describe('Value handling', () => {
53
+ it('should handle string values', () => {
54
+ const { container } = render(
55
+ <Input type="text" value="hello" onChange={onChange} />,
56
+ );
57
+ expect(container.querySelector('input').value).toBe('hello');
58
+ });
59
+
60
+ it('should handle number values', () => {
61
+ const { container } = render(
62
+ <Input type="number" value={42} onChange={onChange} />,
63
+ );
64
+ expect(container.querySelector('input').value).toBe('42');
65
+ });
66
+
67
+ it('should handle empty values', () => {
68
+ const { container } = render(
69
+ <Input type="text" value="" onChange={onChange} />,
70
+ );
71
+ expect(container.querySelector('input').value).toBe('');
72
+ });
73
+
74
+ it('should update when prop changes', () => {
75
+ const { container, rerender } = render(
76
+ <Input type="text" value="initial" onChange={onChange} />,
77
+ );
78
+ expect(container.querySelector('input').value).toBe('initial');
79
+
80
+ rerender(<Input type="text" value="updated" onChange={onChange} />);
81
+ expect(container.querySelector('input').value).toBe('updated');
82
+ });
83
+ });
84
+
85
+ describe('onChange behavior', () => {
86
+ it('should call onChange on input change', async () => {
87
+ const user = userEvent.setup();
88
+ const { container } = render(
89
+ <Input type="text" value="" onChange={onChange} />,
90
+ );
91
+
92
+ const input = container.querySelector('input');
93
+ await user.type(input, 'hello');
94
+
95
+ expect(onChange).toHaveBeenCalled();
96
+ });
97
+
98
+ it('should handle custom error function', async () => {
99
+ const user = userEvent.setup();
100
+ const customError = jest.fn((value) => value.length < 3);
101
+
102
+ const { container } = render(
103
+ <Input type="text" value="" onChange={onChange} error={customError} />,
104
+ );
105
+
106
+ const input = container.querySelector('input');
107
+ await user.type(input, 'ab');
108
+
109
+ expect(customError).toHaveBeenCalled();
110
+ });
111
+ });
112
+
113
+ describe('Error state', () => {
114
+ it('should display error state when validation fails', async () => {
115
+ const user = userEvent.setup();
116
+ const customError = jest.fn(() => true);
117
+
118
+ const { container } = render(
119
+ <Input type="text" value="" onChange={onChange} error={customError} />,
120
+ );
121
+
122
+ const input = container.querySelector('input');
123
+ await user.type(input, 'test');
124
+
125
+ expect(customError).toHaveBeenCalled();
126
+ });
127
+
128
+ it('should clear error state when validation passes', async () => {
129
+ const user = userEvent.setup();
130
+ const customError = jest.fn((value) => !value);
131
+
132
+ const { container } = render(
133
+ <Input type="text" value="" onChange={onChange} error={customError} />,
134
+ );
135
+
136
+ const input = container.querySelector('input');
137
+ await user.type(input, 'test');
138
+
139
+ // After typing valid content, error should be false
140
+ expect(customError).toHaveBeenCalled();
141
+ });
142
+ });
143
+
144
+ describe('Props spreading', () => {
145
+ it('should handle disabled state', () => {
146
+ const { container } = render(
147
+ <Input type="text" value="" onChange={onChange} disabled />,
148
+ );
149
+
150
+ const input = container.querySelector('input');
151
+ expect(input).toBeDisabled();
152
+ });
153
+
154
+ it('should handle readonly state', () => {
155
+ const { container } = render(
156
+ <Input type="text" value="readonly" onChange={onChange} readOnly />,
157
+ );
158
+
159
+ const input = container.querySelector('input');
160
+ expect(input).toHaveAttribute('readonly');
161
+ });
162
+ });
163
+
164
+ describe('Default props', () => {
165
+ it('should use default type of text', () => {
166
+ const { container } = render(
167
+ <Input value="" onChange={onChange} />,
168
+ );
169
+
170
+ const input = container.querySelector('input');
171
+ expect(input.type).toBe('text');
172
+ });
173
+
174
+ it('should use default error function that validates number type', () => {
175
+ const { container } = render(
176
+ <Input type="number" value="" onChange={onChange} />,
177
+ );
178
+
179
+ const input = container.querySelector('input');
180
+ expect(input).toBeInTheDocument();
181
+ });
182
+
183
+ it('should have noModelUpdateOnError default to false', () => {
184
+ const { container } = render(
185
+ <Input type="text" value="" onChange={onChange} />,
186
+ );
187
+
188
+ const input = container.querySelector('input');
189
+ expect(input).toBeInTheDocument();
190
+ });
191
+ });
192
+ });
@@ -0,0 +1,457 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+ import { Langs, LanguageControls } from '../langs';
5
+
6
+ describe('Langs Component', () => {
7
+ let onChange;
8
+ const renderComponent = (extras = {}) => {
9
+ const defaults = {
10
+ uid: '1',
11
+ onChange,
12
+ langs: ['en-US', 'es-ES'],
13
+ selected: 'en-US',
14
+ };
15
+ const props = { ...defaults, ...extras };
16
+ return render(<Langs {...props} />);
17
+ };
18
+
19
+ beforeEach(() => {
20
+ onChange = jest.fn();
21
+ });
22
+
23
+ describe('rendering', () => {
24
+ it('renders language selector with options', () => {
25
+ renderComponent();
26
+
27
+ // Select should be present
28
+ const select = screen.getByRole('combobox');
29
+ expect(select).toBeInTheDocument();
30
+
31
+ // Should show selected value - MUI Select displays value as text content
32
+ expect(select).toHaveTextContent('en-US');
33
+ });
34
+
35
+ it('renders with custom label', () => {
36
+ renderComponent({ label: 'Choose Language' });
37
+ expect(screen.getByText('Choose Language')).toBeInTheDocument();
38
+ });
39
+
40
+ it('renders all language options', () => {
41
+ const { container } = renderComponent({
42
+ langs: ['en-US', 'es-ES', 'fr-FR', 'de-DE'],
43
+ });
44
+
45
+ const select = screen.getByRole('combobox');
46
+ expect(select).toBeInTheDocument();
47
+ });
48
+
49
+ it('renders with multiple languages', () => {
50
+ const langs = ['en-US', 'es-ES', 'fr-FR', 'pt-BR'];
51
+ renderComponent({ langs });
52
+
53
+ const select = screen.getByRole('combobox');
54
+ expect(select).toHaveTextContent('en-US');
55
+ });
56
+
57
+ it('renders FormControl with proper structure', () => {
58
+ const { container } = renderComponent();
59
+
60
+ expect(container.querySelector('[class*="MuiFormControl"]')).toBeInTheDocument();
61
+ });
62
+ });
63
+
64
+ describe('user interactions', () => {
65
+ it('calls onChange when user selects a language', async () => {
66
+ const user = userEvent.setup();
67
+ renderComponent();
68
+
69
+ const select = screen.getByRole('combobox');
70
+
71
+ // Open the select and choose es-ES
72
+ await user.click(select);
73
+ await user.click(screen.getByRole('option', { name: 'es-ES' }));
74
+
75
+ expect(onChange).toHaveBeenCalledWith('es-ES');
76
+ });
77
+
78
+ it('calls onChange with correct value', async () => {
79
+ const user = userEvent.setup();
80
+ renderComponent({ selected: 'es-ES' });
81
+
82
+ const select = screen.getByRole('combobox');
83
+
84
+ await user.click(select);
85
+ await user.click(screen.getByRole('option', { name: 'en-US' }));
86
+
87
+ expect(onChange).toHaveBeenCalledWith('en-US');
88
+ });
89
+
90
+ it('opens dropdown on click', async () => {
91
+ const user = userEvent.setup();
92
+ renderComponent({
93
+ langs: ['en-US', 'es-ES', 'fr-FR'],
94
+ });
95
+
96
+ const select = screen.getByRole('combobox');
97
+ await user.click(select);
98
+
99
+ expect(screen.getByRole('option', { name: 'en-US' })).toBeInTheDocument();
100
+ expect(screen.getByRole('option', { name: 'es-ES' })).toBeInTheDocument();
101
+ expect(screen.getByRole('option', { name: 'fr-FR' })).toBeInTheDocument();
102
+ });
103
+
104
+ it('does not call onChange if no onChange prop provided', async () => {
105
+ const user = userEvent.setup();
106
+ const { container } = render(
107
+ <Langs
108
+ uid="1"
109
+ langs={['en-US', 'es-ES']}
110
+ selected="en-US"
111
+ />,
112
+ );
113
+
114
+ const select = screen.getByRole('combobox');
115
+ await user.click(select);
116
+ await user.click(screen.getByRole('option', { name: 'es-ES' }));
117
+
118
+ // Should not throw any error
119
+ expect(select).toBeInTheDocument();
120
+ });
121
+ });
122
+
123
+ describe('props handling', () => {
124
+ it('accepts custom uid', () => {
125
+ const customUid = 'custom-uid-123';
126
+ const { container } = renderComponent({ uid: customUid });
127
+
128
+ const input = container.querySelector(`#${customUid}`);
129
+ expect(input).toBeInTheDocument();
130
+ });
131
+
132
+ it('generates random uid if not provided', () => {
133
+ const { container } = render(
134
+ <Langs
135
+ onChange={onChange}
136
+ langs={['en-US', 'es-ES']}
137
+ selected="en-US"
138
+ />,
139
+ );
140
+
141
+ const select = screen.getByRole('combobox');
142
+ expect(select).toBeInTheDocument();
143
+ });
144
+
145
+ it('handles empty label', () => {
146
+ renderComponent({ label: '' });
147
+
148
+ const select = screen.getByRole('combobox');
149
+ expect(select).toBeInTheDocument();
150
+ });
151
+
152
+ it('handles very long label', () => {
153
+ const longLabel = 'Choose the language you would like to use for editing this content';
154
+ renderComponent({ label: longLabel });
155
+
156
+ expect(screen.getByText(longLabel)).toBeInTheDocument();
157
+ });
158
+ });
159
+
160
+ describe('value updates', () => {
161
+ it('updates selected value when prop changes', () => {
162
+ const { rerender } = renderComponent({ selected: 'en-US' });
163
+
164
+ let select = screen.getByRole('combobox');
165
+ expect(select).toHaveTextContent('en-US');
166
+
167
+ rerender(
168
+ <Langs
169
+ uid="1"
170
+ onChange={onChange}
171
+ langs={['en-US', 'es-ES']}
172
+ selected="es-ES"
173
+ />,
174
+ );
175
+
176
+ select = screen.getByRole('combobox');
177
+ expect(select).toHaveTextContent('es-ES');
178
+ });
179
+
180
+ it('maintains state when language options change', async () => {
181
+ const user = userEvent.setup();
182
+ const { rerender } = renderComponent({
183
+ langs: ['en-US', 'es-ES'],
184
+ selected: 'en-US',
185
+ });
186
+
187
+ rerender(
188
+ <Langs
189
+ uid="1"
190
+ onChange={onChange}
191
+ langs={['en-US', 'es-ES', 'fr-FR']}
192
+ selected="en-US"
193
+ />,
194
+ );
195
+
196
+ const select = screen.getByRole('combobox');
197
+ expect(select).toHaveTextContent('en-US');
198
+ });
199
+ });
200
+
201
+ describe('keyboard navigation', () => {
202
+ it('opens dropdown with keyboard', async () => {
203
+ const user = userEvent.setup();
204
+ renderComponent({
205
+ langs: ['en-US', 'es-ES'],
206
+ });
207
+
208
+ const select = screen.getByRole('combobox');
209
+ select.focus();
210
+
211
+ await user.keyboard(' ');
212
+
213
+ expect(screen.getByRole('option', { name: 'en-US' })).toBeInTheDocument();
214
+ });
215
+ });
216
+
217
+ describe('accessibility', () => {
218
+ it('has proper label association', () => {
219
+ renderComponent({ label: 'Select Language' });
220
+
221
+ const select = screen.getByRole('combobox');
222
+ expect(select).toBeInTheDocument();
223
+ });
224
+
225
+ it('renders options with proper roles', async () => {
226
+ const user = userEvent.setup();
227
+ renderComponent({
228
+ langs: ['en-US', 'es-ES', 'fr-FR'],
229
+ });
230
+
231
+ const select = screen.getByRole('combobox');
232
+ await user.click(select);
233
+
234
+ const options = screen.getAllByRole('option');
235
+ expect(options.length).toBe(3);
236
+ });
237
+ });
238
+ });
239
+
240
+ describe('LanguageControls Component', () => {
241
+ const defaultProps = {
242
+ langs: ['en-US', 'es-ES', 'fr-FR'],
243
+ activeLang: 'en-US',
244
+ defaultLang: 'en-US',
245
+ onActiveLangChange: jest.fn(),
246
+ onDefaultLangChange: jest.fn(),
247
+ };
248
+
249
+ beforeEach(() => {
250
+ jest.clearAllMocks();
251
+ });
252
+
253
+ describe('rendering', () => {
254
+ it('renders both language selectors', () => {
255
+ render(<LanguageControls {...defaultProps} />);
256
+
257
+ expect(screen.getByText('Choose language to edit')).toBeInTheDocument();
258
+ expect(screen.getByText('Default language')).toBeInTheDocument();
259
+ });
260
+
261
+ it('renders with correct initial values', () => {
262
+ render(<LanguageControls {...defaultProps} />);
263
+
264
+ const selects = screen.getAllByRole('combobox');
265
+ expect(selects).toHaveLength(2);
266
+ });
267
+
268
+ it('applies custom className', () => {
269
+ const { container } = render(
270
+ <LanguageControls {...defaultProps} className="custom-class" />,
271
+ );
272
+
273
+ expect(container.querySelector('.custom-class')).toBeInTheDocument();
274
+ });
275
+ });
276
+
277
+ describe('user interactions', () => {
278
+ it('calls onActiveLangChange when active language is changed', async () => {
279
+ const user = userEvent.setup();
280
+ const onActiveLangChange = jest.fn();
281
+
282
+ render(
283
+ <LanguageControls
284
+ {...defaultProps}
285
+ activeLang="en-US"
286
+ onActiveLangChange={onActiveLangChange}
287
+ />,
288
+ );
289
+
290
+ const selects = screen.getAllByRole('combobox');
291
+ const activeSelect = selects[0];
292
+
293
+ await user.click(activeSelect);
294
+ await user.click(screen.getByRole('option', { name: 'es-ES' }));
295
+
296
+ expect(onActiveLangChange).toHaveBeenCalledWith('es-ES');
297
+ });
298
+
299
+ it('calls onDefaultLangChange when default language is changed', async () => {
300
+ const user = userEvent.setup();
301
+ const onDefaultLangChange = jest.fn();
302
+
303
+ render(
304
+ <LanguageControls
305
+ {...defaultProps}
306
+ defaultLang="en-US"
307
+ onDefaultLangChange={onDefaultLangChange}
308
+ />,
309
+ );
310
+
311
+ const selects = screen.getAllByRole('combobox');
312
+ const defaultSelect = selects[1];
313
+
314
+ await user.click(defaultSelect);
315
+ await user.click(screen.getByRole('option', { name: 'fr-FR' }));
316
+
317
+ expect(onDefaultLangChange).toHaveBeenCalledWith('fr-FR');
318
+ });
319
+
320
+ it('handles independent language changes', async () => {
321
+ const user = userEvent.setup();
322
+ const onActiveLangChange = jest.fn();
323
+ const onDefaultLangChange = jest.fn();
324
+
325
+ render(
326
+ <LanguageControls
327
+ {...defaultProps}
328
+ activeLang="en-US"
329
+ defaultLang="en-US"
330
+ onActiveLangChange={onActiveLangChange}
331
+ onDefaultLangChange={onDefaultLangChange}
332
+ />,
333
+ );
334
+
335
+ const selects = screen.getAllByRole('combobox');
336
+
337
+ // Change active language
338
+ await user.click(selects[0]);
339
+ await user.click(screen.getByRole('option', { name: 'es-ES' }));
340
+
341
+ expect(onActiveLangChange).toHaveBeenCalledWith('es-ES');
342
+ expect(onDefaultLangChange).not.toHaveBeenCalled();
343
+ });
344
+ });
345
+
346
+ describe('props handling', () => {
347
+ it('displays correct language options', async () => {
348
+ const user = userEvent.setup();
349
+ render(
350
+ <LanguageControls
351
+ {...defaultProps}
352
+ langs={['en-US', 'es-ES', 'fr-FR', 'de-DE']}
353
+ />,
354
+ );
355
+
356
+ const selects = screen.getAllByRole('combobox');
357
+ await user.click(selects[0]);
358
+
359
+ expect(screen.getByRole('option', { name: 'en-US' })).toBeInTheDocument();
360
+ expect(screen.getByRole('option', { name: 'es-ES' })).toBeInTheDocument();
361
+ expect(screen.getByRole('option', { name: 'fr-FR' })).toBeInTheDocument();
362
+ expect(screen.getByRole('option', { name: 'de-DE' })).toBeInTheDocument();
363
+ });
364
+
365
+ it('maintains different active and default languages', async () => {
366
+ const user = userEvent.setup();
367
+ render(
368
+ <LanguageControls
369
+ {...defaultProps}
370
+ activeLang="es-ES"
371
+ defaultLang="en-US"
372
+ />,
373
+ );
374
+
375
+ const selects = screen.getAllByRole('combobox');
376
+ expect(selects[0]).toHaveTextContent('es-ES');
377
+ expect(selects[1]).toHaveTextContent('en-US');
378
+ });
379
+ });
380
+
381
+ describe('updates', () => {
382
+ it('updates when activeLang prop changes', () => {
383
+ const { rerender } = render(
384
+ <LanguageControls
385
+ {...defaultProps}
386
+ activeLang="en-US"
387
+ />,
388
+ );
389
+
390
+ let selects = screen.getAllByRole('combobox');
391
+ expect(selects[0]).toHaveTextContent('en-US');
392
+
393
+ rerender(
394
+ <LanguageControls
395
+ {...defaultProps}
396
+ activeLang="es-ES"
397
+ />,
398
+ );
399
+
400
+ selects = screen.getAllByRole('combobox');
401
+ expect(selects[0]).toHaveTextContent('es-ES');
402
+ });
403
+
404
+ it('updates when defaultLang prop changes', () => {
405
+ const { rerender } = render(
406
+ <LanguageControls
407
+ {...defaultProps}
408
+ defaultLang="en-US"
409
+ />,
410
+ );
411
+
412
+ let selects = screen.getAllByRole('combobox');
413
+ expect(selects[1]).toHaveTextContent('en-US');
414
+
415
+ rerender(
416
+ <LanguageControls
417
+ {...defaultProps}
418
+ defaultLang="fr-FR"
419
+ />,
420
+ );
421
+
422
+ selects = screen.getAllByRole('combobox');
423
+ expect(selects[1]).toHaveTextContent('fr-FR');
424
+ });
425
+
426
+ it('updates when langs prop changes', async () => {
427
+ const user = userEvent.setup();
428
+ const { rerender } = render(
429
+ <LanguageControls
430
+ {...defaultProps}
431
+ langs={['en-US', 'es-ES']}
432
+ />,
433
+ );
434
+
435
+ rerender(
436
+ <LanguageControls
437
+ {...defaultProps}
438
+ langs={['en-US', 'es-ES', 'fr-FR']}
439
+ />,
440
+ );
441
+
442
+ const selects = screen.getAllByRole('combobox');
443
+ await user.click(selects[0]);
444
+
445
+ expect(screen.getByRole('option', { name: 'fr-FR' })).toBeInTheDocument();
446
+ });
447
+ });
448
+
449
+ describe('accessibility', () => {
450
+ it('has proper semantic structure', () => {
451
+ const { container } = render(<LanguageControls {...defaultProps} />);
452
+
453
+ const selects = screen.getAllByRole('combobox');
454
+ expect(selects.length).toBe(2);
455
+ });
456
+ });
457
+ });