1- /** @vitest -environment node */
1+ /** @vitest -environment jsdom */
22/** biome-ignore assist/source/organizeImports: Preserve the documented core/external/UI import order. */
3+ import { act , createRef } from 'react'
4+ import { createRoot } from 'react-dom/client'
35import { renderToStaticMarkup } from 'react-dom/server'
46import { describe , expect , it } from 'vitest'
57import { Button } from '@sim/emcn'
@@ -22,6 +24,9 @@ describe('Button iconSize', () => {
2224 for ( const [ iconSize , previousClass ] of [
2325 [ 'compact' , 'size-6 p-0' ] ,
2426 [ 'compact-fixed' , 'size-[24px] p-0' ] ,
27+ [ 'regular' , 'size-7 p-0' ] ,
28+ [ 'roomy' , 'size-8 p-0' ] ,
29+ [ 'touch' , 'size-10 p-0' ] ,
2530 ] as const ) {
2631 it . each ( TREATMENTS ) (
2732 `preserves the ${ iconSize } treatment with size=$size and variant=$variant` ,
@@ -65,3 +70,82 @@ describe('Button iconSize', () => {
6570 expect ( markup ) . not . toContain ( 'iconPadding' )
6671 } )
6772} )
73+
74+ describe ( 'Button shared action geometry' , ( ) => {
75+ it ( 'composes responsive geometry, explicit padding and round shape without changing icon treatment' , ( ) => {
76+ const markup = renderToStaticMarkup (
77+ < Button
78+ variant = 'ghost'
79+ size = 'icon'
80+ iconSize = { { base : 'touch' , sm : 'regular' } }
81+ iconPadding = 'sm'
82+ shape = 'round'
83+ aria-label = 'Edit'
84+ >
85+ < svg strokeWidth = { 1.55 } />
86+ </ Button >
87+ )
88+ expect ( markup ) . toContain ( 'size-10' )
89+ expect ( markup ) . toContain ( 'sm:size-7' )
90+ expect ( markup ) . not . toContain ( 'size-[20px]' )
91+ expect ( markup ) . toContain ( 'p-1' )
92+ expect ( markup ) . not . toContain ( 'p-0' )
93+ expect ( markup ) . toContain ( 'rounded-full' )
94+ expect ( markup ) . not . toContain ( 'rounded-sm' )
95+ expect ( markup ) . toContain ( '[stroke-width:1.25]' )
96+ expect ( markup ) . toContain ( 'text-[var(--text-icon-muted)]' )
97+ expect ( markup ) . not . toMatch ( / (?: i c o n S i z e | i c o n P a d d i n g | s h a p e ) = / )
98+ } )
99+
100+ it ( 'retains an inline caption size and supports a base-only responsive value' , ( ) => {
101+ const inline = renderToStaticMarkup ( < Button size = 'inline' > Generate</ Button > )
102+ expect ( inline ) . toContain ( 'h-[20px]' )
103+ expect ( inline ) . toContain ( 'text-caption' )
104+ expect ( inline ) . toContain ( 'px-1.5 py-0' )
105+ const baseOnly = renderToStaticMarkup ( < Button iconSize = { { base : 'roomy' } } aria-label = 'Run' /> )
106+ expect ( baseOnly ) . toContain ( 'size-8' )
107+ expect ( baseOnly ) . not . toContain ( 'sm:size' )
108+ } )
109+
110+ it ( 'forwards refs and native focus, submission and disabled behavior with responsive sizing' , ( ) => {
111+ const container = document . createElement ( 'div' )
112+ document . body . appendChild ( container )
113+ const root = createRoot ( container )
114+ const ref = createRef < HTMLButtonElement > ( )
115+ let submissions = 0
116+ ; ( globalThis as { IS_REACT_ACT_ENVIRONMENT ?: boolean } ) . IS_REACT_ACT_ENVIRONMENT = true
117+ const render = ( disabled : boolean ) =>
118+ act ( ( ) =>
119+ root . render (
120+ < form
121+ onSubmit = { ( event ) => {
122+ event . preventDefault ( )
123+ submissions ++
124+ } }
125+ >
126+ < Button
127+ ref = { ref }
128+ type = 'submit'
129+ iconSize = { { base : 'touch' , sm : 'regular' } }
130+ disabled = { disabled }
131+ aria-label = 'Apply'
132+ />
133+ </ form >
134+ )
135+ )
136+ try {
137+ render ( false )
138+ ref . current ?. focus ( )
139+ expect ( document . activeElement ) . toBe ( ref . current )
140+ act ( ( ) => ref . current ?. click ( ) )
141+ expect ( submissions ) . toBe ( 1 )
142+ render ( true )
143+ act ( ( ) => ref . current ?. click ( ) )
144+ expect ( submissions ) . toBe ( 1 )
145+ expect ( ref . current ?. disabled ) . toBe ( true )
146+ } finally {
147+ act ( ( ) => root . unmount ( ) )
148+ container . remove ( )
149+ }
150+ } )
151+ } )
0 commit comments