|
8 | 8 |
|
9 | 9 | import { describe, it, expect } from 'vitest'; |
10 | 10 | import { render, screen } from '@testing-library/react'; |
11 | | -import { DetailSection } from '../DetailSection'; |
| 11 | +import { DetailSection, getResponsiveSpanClass } from '../DetailSection'; |
12 | 12 |
|
13 | 13 | describe('DetailSection', () => { |
14 | 14 | it('should render text fields as plain text', () => { |
@@ -317,4 +317,113 @@ describe('DetailSection', () => { |
317 | 317 | // Should use 'text' renderer, not 'number' |
318 | 318 | expect(screen.getByText('Alice')).toBeInTheDocument(); |
319 | 319 | }); |
| 320 | + |
| 321 | + it('should use responsive span classes for wide fields in 3-column layout', () => { |
| 322 | + const section = { |
| 323 | + title: 'Wide Fields', |
| 324 | + fields: Array.from({ length: 12 }, (_, i) => ({ |
| 325 | + name: `field_${i}`, |
| 326 | + label: `Field ${i}`, |
| 327 | + type: i === 5 ? 'textarea' : 'text', |
| 328 | + })), |
| 329 | + }; |
| 330 | + const { container } = render( |
| 331 | + <DetailSection section={section} data={{}} /> |
| 332 | + ); |
| 333 | + const grid = container.querySelector('.grid'); |
| 334 | + expect(grid).toBeTruthy(); |
| 335 | + expect(grid!.className).toContain('lg:grid-cols-3'); |
| 336 | + // Wide field (textarea) should have responsive span, not bare col-span-3 |
| 337 | + const fields = container.querySelectorAll('[class*="col-span"]'); |
| 338 | + fields.forEach((field) => { |
| 339 | + // No bare col-span-3 at base level — must be lg: prefixed |
| 340 | + const classes = field.className.split(/\s+/); |
| 341 | + const hasBareSpan3 = classes.some((c: string) => c === 'col-span-3'); |
| 342 | + expect(hasBareSpan3).toBe(false); |
| 343 | + }); |
| 344 | + }); |
| 345 | + |
| 346 | + it('should use responsive span classes for wide fields in 2-column layout', () => { |
| 347 | + const section = { |
| 348 | + title: 'Wide Fields', |
| 349 | + fields: [ |
| 350 | + { name: 'a', label: 'A', type: 'text' }, |
| 351 | + { name: 'b', label: 'B', type: 'text' }, |
| 352 | + { name: 'c', label: 'C', type: 'text' }, |
| 353 | + { name: 'd', label: 'D', type: 'text' }, |
| 354 | + { name: 'notes', label: 'Notes', type: 'textarea' }, |
| 355 | + ], |
| 356 | + }; |
| 357 | + const { container } = render( |
| 358 | + <DetailSection section={section} data={{}} /> |
| 359 | + ); |
| 360 | + const grid = container.querySelector('.grid'); |
| 361 | + expect(grid!.className).toContain('md:grid-cols-2'); |
| 362 | + // Wide field should have md:col-span-2, not bare col-span-2 |
| 363 | + const fields = container.querySelectorAll('[class*="col-span"]'); |
| 364 | + fields.forEach((field) => { |
| 365 | + const classes = field.className.split(/\s+/); |
| 366 | + const hasBareSpan2 = classes.some((c: string) => c === 'col-span-2'); |
| 367 | + expect(hasBareSpan2).toBe(false); |
| 368 | + }); |
| 369 | + }); |
| 370 | + |
| 371 | + it('should not apply col-span at base breakpoint to prevent implicit grid columns on mobile', () => { |
| 372 | + const section = { |
| 373 | + title: 'Mobile Safe', |
| 374 | + fields: Array.from({ length: 15 }, (_, i) => ({ |
| 375 | + name: `field_${i}`, |
| 376 | + label: `Field ${i}`, |
| 377 | + type: i === 0 ? 'textarea' : 'text', |
| 378 | + })), |
| 379 | + }; |
| 380 | + const { container } = render( |
| 381 | + <DetailSection section={section} data={{}} /> |
| 382 | + ); |
| 383 | + // Ensure no bare col-span-N (N>1) classes without responsive prefix |
| 384 | + const allElements = container.querySelectorAll('*'); |
| 385 | + allElements.forEach((el) => { |
| 386 | + const classes = el.className?.split?.(/\s+/) || []; |
| 387 | + classes.forEach((cls: string) => { |
| 388 | + if (cls.match(/^col-span-[2-9]$/)) { |
| 389 | + throw new Error(`Found bare "${cls}" class without responsive prefix — would break mobile single-column layout`); |
| 390 | + } |
| 391 | + }); |
| 392 | + }); |
| 393 | + }); |
| 394 | +}); |
| 395 | + |
| 396 | +describe('getResponsiveSpanClass', () => { |
| 397 | + it('should return empty string for no span', () => { |
| 398 | + expect(getResponsiveSpanClass(undefined, 2)).toBe(''); |
| 399 | + }); |
| 400 | + |
| 401 | + it('should return empty string for span=1', () => { |
| 402 | + expect(getResponsiveSpanClass(1, 3)).toBe(''); |
| 403 | + }); |
| 404 | + |
| 405 | + it('should return empty string for 1-column layout', () => { |
| 406 | + expect(getResponsiveSpanClass(3, 1)).toBe(''); |
| 407 | + }); |
| 408 | + |
| 409 | + it('should return md:col-span-2 for span=2 in 2-column layout', () => { |
| 410 | + expect(getResponsiveSpanClass(2, 2)).toBe('md:col-span-2'); |
| 411 | + }); |
| 412 | + |
| 413 | + it('should cap span to 2 in 2-column layout', () => { |
| 414 | + expect(getResponsiveSpanClass(3, 2)).toBe('md:col-span-2'); |
| 415 | + expect(getResponsiveSpanClass(6, 2)).toBe('md:col-span-2'); |
| 416 | + }); |
| 417 | + |
| 418 | + it('should return md:col-span-2 for span=2 in 3-column layout', () => { |
| 419 | + expect(getResponsiveSpanClass(2, 3)).toBe('md:col-span-2'); |
| 420 | + }); |
| 421 | + |
| 422 | + it('should return responsive classes for span=3 in 3-column layout', () => { |
| 423 | + expect(getResponsiveSpanClass(3, 3)).toBe('md:col-span-2 lg:col-span-3'); |
| 424 | + }); |
| 425 | + |
| 426 | + it('should cap span to 3 in 3-column layout', () => { |
| 427 | + expect(getResponsiveSpanClass(6, 3)).toBe('md:col-span-2 lg:col-span-3'); |
| 428 | + }); |
320 | 429 | }); |
0 commit comments