1- import { composeStories } from '@storybook/react' ;
2- import '@testing-library/jest-dom' ;
31import { render , screen } from '@testing-library/react' ;
42import fc from 'fast-check' ;
5- import { describe , expect , it , vi } from 'vitest' ;
3+ import { describe , expect , it } from 'vitest' ;
64import { fuzzComponent } from '../../../tests/utils/fuzzComponent' ;
75import { Button , ButtonProps } from './Button' ;
8- import * as stories from './Button.stories' ;
96
10- // Compose all stories from the Button.stories file for testing
11- const { Primary } = composeStories ( stories ) ;
12-
13- // Helper to get only valid story components (skip non-stories and metadata)
14- type StoryComponent = React . FC < Record < string , unknown > > & {
15- args ?: ButtonProps ;
16- } ;
17- const getValidStories = ( storiesObj : Record < string , unknown > ) =>
18- Object . entries ( composeStories ( storiesObj ) ) . filter (
19- ( [ key , Story ] ) =>
20- key !== 'default' && key !== '__esModule' && typeof Story === 'function' ,
21- ) as [ string , StoryComponent ] [ ] ;
22-
23- // --- Storybook Stories Test Suite ---
24- describe ( 'Button (Storybook stories)' , ( ) => {
25- // Dynamically test all valid stories for rendering and label
26- getValidStories ( stories ) . forEach ( ( [ storyName , Story ] ) => {
27- it ( `renders ${ storyName } button with default args` , ( ) => {
28- // Render the story as a component
29- render ( < Story /> ) ;
30- // Use the label from story args, fallback to 'Button' if not set
31- const label = ( Story . args && Story . args . label ) || 'Button' ;
32- // Check that the label is rendered
33- expect ( screen . getByText ( label ) ) . toBeInTheDocument ( ) ;
34- // Check for accessible role
35- expect ( screen . getByRole ( 'button' ) ) . toBeInTheDocument ( ) ;
36- } ) ;
7+ describe ( 'Button: Unit Test' , ( ) => {
8+ it ( 'applies mds class name' , ( ) => {
9+ render ( < Button label = "Button" /> ) ;
10+ expect ( screen . getByRole ( 'button' ) ) . toHaveClass ( 'mds-btn' ) ;
3711 } ) ;
3812
39- // Explicit tests for the Primary story with custom props and edge cases
40- it ( 'renders Primary button with overridden label' , ( ) => {
41- // Test that the label prop overrides the default label
42- render ( < Primary label = "Hello world" /> ) ;
43- expect ( screen . getByText ( 'Hello world' ) ) . toBeInTheDocument ( ) ;
13+ it ( 'renders with empty label' , ( ) => {
14+ render ( < Button label = "" /> ) ;
15+ expect ( screen . getByRole ( 'button' ) ) . toBeInTheDocument ( ) ;
4416 } ) ;
4517
46- it ( 'calls onClick handler when clicked' , ( ) => {
47- // Test that the onClick handler is called when the button is clicked
48- const handleClick = vi . fn ( ) ;
49- render ( < Primary onClick = { handleClick } /> ) ;
50- screen . getByRole ( 'button' ) . click ( ) ;
51- expect ( handleClick ) . toHaveBeenCalledTimes ( 1 ) ;
18+ it ( 'renders the label correctly' , ( ) => {
19+ render ( < Button label = "ThisIsAButton" /> ) ;
20+ expect ( screen . getByRole ( 'button' ) ) . toHaveTextContent ( 'ThisIsAButton' ) ;
5221 } ) ;
5322
54- it ( 'does not call onClick when disabled' , ( ) => {
55- // Test that the onClick handler is not called when the button is disabled
56- const handleClick = vi . fn ( ) ;
57- render ( < Primary disabled onClick = { handleClick } /> ) ;
58- screen . getByRole ( 'button' ) . click ( ) ;
59- expect ( handleClick ) . not . toHaveBeenCalled ( ) ;
23+ it ( 'applies variant prop' , ( ) => {
24+ render ( < Button label = "Button" variant = "secondary" /> ) ;
25+ expect ( screen . getByRole ( 'button' ) ) . toHaveClass ( 'btn-secondary' ) ; // the class applied by react-bootstrap
6026 } ) ;
6127
62- it ( 'applies the correct variant and size classes' , ( ) => {
63- // Test that the correct CSS classes are applied for variant and size
64- render ( < Primary variant = "danger" size = "lg" /> ) ;
65- const btn = screen . getByRole ( 'button' ) ;
66- expect ( btn . className ) . toMatch ( / b t n - d a n g e r / ) ;
67- expect ( btn . className ) . toMatch ( / b t n - l g / ) ;
28+ it ( 'handles invalid variant prop as default variant' , ( ) => {
29+ render ( < Button label = "Button" variant = "invalid" /> ) ;
30+ expect ( screen . getByRole ( 'button' ) ) . toHaveClass ( 'btn-primary' ) ; // the class applied by react-bootstrap
6831 } ) ;
6932
70- it ( 'forwards aria-label and other aria props' , ( ) => {
71- // Test that aria-label and other aria props are forwarded to the button
72- render ( < Primary aria-label = "custom label" /> ) ;
73- expect ( screen . getByLabelText ( 'custom label' ) ) . toBeInTheDocument ( ) ;
33+ it ( 'applies the size classes' , ( ) => {
34+ render ( < Button label = "Button" size = "lg" /> ) ;
35+ expect ( screen . getByRole ( 'button' ) ) . toHaveClass ( 'btn-lg' ) ;
7436 } ) ;
7537
76- it ( 'renders with the correct type attribute' , ( ) => {
77- // Test that the type attribute is set correctly
78- render ( < Primary type = "submit" /> ) ;
79- expect ( screen . getByRole ( 'button' ) ) . toHaveAttribute ( 'type' , 'submit' ) ;
38+ it ( 'respects disabled prop' , ( ) => {
39+ render ( < Button label = "Button" disabled /> ) ;
40+ expect ( screen . getByRole ( 'button' ) ) . toBeDisabled ( ) ;
8041 } ) ;
8142
8243 it ( 'forwards extra props to the button element' , ( ) => {
83- // Test that extra props like data-testid are forwarded to the button
84- render ( < Primary data-testid = "my-btn" /> ) ;
44+ render ( < Button label = "Button" data-testid = "my-btn" /> ) ;
8545 expect ( screen . getByTestId ( 'my-btn' ) ) . toBeInTheDocument ( ) ;
8646 } ) ;
8747
88- it ( 'respects disabled prop' , ( ) => {
89- render ( < Primary disabled /> ) ;
90- const btn = screen . getByRole ( 'button' ) ;
91- expect ( btn ) . toBeDisabled ( ) ;
92- } ) ;
93-
94- it ( 'renders with empty label' , ( ) => {
95- render ( < Primary label = "" /> ) ;
96- // Should still render a button
97- expect ( screen . getByRole ( 'button' ) ) . toBeInTheDocument ( ) ;
98- } ) ;
99-
100- it ( 'renders with long label' , ( ) => {
101- const longLabel = 'L' . repeat ( 100 ) ;
102- render ( < Primary label = { longLabel } /> ) ;
103- expect ( screen . getByText ( longLabel ) ) . toBeInTheDocument ( ) ;
104- } ) ;
105-
106- it ( 'renders with special characters in label' , ( ) => {
107- const special = "!@#$%^&*()_+-=[]{}|;:'<>,.?/" ;
108- render ( < Primary label = { special } /> ) ;
109- expect ( screen . getByText ( special ) ) . toBeInTheDocument ( ) ;
110- } ) ;
111- } ) ;
112-
113- // --- Fuzz Testing Suite ---
114- describe ( 'Fuzz Button' , ( ) => {
115- // Fuzz with each valid story's args as a seed to ensure real-world coverage
116- getValidStories ( stories ) . forEach ( ( [ storyName , Story ] ) => {
117- if ( Story . args ) {
118- it ( `fuzzes Button with story args for ${ storyName } ` , ( ) => {
119- // Use the story's args as a fixed input for the fuzzer to ensure all real-world story props are tested
120- fuzzComponent (
121- Button ,
122- fc . constant ( Story . args ) ,
123- ( props : ButtonProps ) => props . label ,
124- { numRuns : 1 } ,
125- ) ;
126- } ) ;
127- }
128- } ) ;
129-
130- // Fuzz with 100 random prop combinations for broad edge case coverage
131- it ( 'should render and display label for random props' , ( ) => {
132- // Use fast-check to generate 100 random prop combinations for Button
48+ it ( 'renders and display label for random props' , ( ) => {
13349 fuzzComponent (
13450 Button ,
13551 fc . record < ButtonProps > ( {
@@ -149,10 +65,10 @@ describe('Fuzz Button', () => {
14965 'outline-secondary' ,
15066 'outline-danger' ,
15167 ) as unknown as fc . Arbitrary < ButtonProps [ 'variant' ] > ,
68+ disabled : fc . boolean ( ) ,
15269 size : fc . option ( fc . constantFrom ( 'sm' , 'lg' ) , {
15370 nil : undefined ,
15471 } ) as unknown as fc . Arbitrary < ButtonProps [ 'size' ] > ,
155- disabled : fc . boolean ( ) ,
15672 } ) ,
15773 ( props : ButtonProps ) => props . label ,
15874 { numRuns : 100 } ,
0 commit comments