1- import React , { type FC , Fragment , useCallback , useRef , useState } from 'react' ;
1+ import React , { type FC , useCallback , useRef , useState } from 'react' ;
22
3- import { Button } from 'storybook/internal/components' ;
3+ import { Button , ListItem } from 'storybook/internal/components' ;
44import {
55 TESTING_MODULE_CONFIG_CHANGE ,
66 type TestProviderConfig ,
77 type TestProviderState ,
8- type TestingModuleConfigChangePayload ,
98} from 'storybook/internal/core-events' ;
109import type { API } from 'storybook/internal/manager-api' ;
11- import { styled } from 'storybook/internal/theming' ;
10+ import { styled , useTheme } from 'storybook/internal/theming' ;
1211
13- import { EditIcon , EyeIcon , PlayHollowIcon , StopAltHollowIcon } from '@storybook/icons' ;
12+ import {
13+ AccessibilityIcon ,
14+ EditIcon ,
15+ EyeIcon ,
16+ PlayHollowIcon ,
17+ PointerHandIcon ,
18+ ShieldIcon ,
19+ StopAltHollowIcon ,
20+ } from '@storybook/icons' ;
21+
22+ import { isEqual } from 'es-toolkit' ;
23+ import { debounce } from 'es-toolkit/compat' ;
1424
1525import { type Config , type Details , TEST_PROVIDER_ID } from '../constants' ;
1626import { Description } from './Description' ;
1727import { GlobalErrorModal } from './GlobalErrorModal' ;
28+ import { TestStatusIcon } from './TestStatusIcon' ;
29+
30+ const Container = styled . div ( {
31+ display : 'flex' ,
32+ flexDirection : 'column' ,
33+ } ) ;
34+
35+ const Heading = styled . div ( {
36+ display : 'flex' ,
37+ justifyContent : 'space-between' ,
38+ padding : '8px 2px' ,
39+ gap : 6 ,
40+ } ) ;
1841
1942const Info = styled . div ( {
2043 display : 'flex' ,
@@ -33,32 +56,37 @@ const Actions = styled.div({
3356 gap : 6 ,
3457} ) ;
3558
36- const Head = styled . div ( {
37- display : 'flex' ,
38- justifyContent : 'space-between' ,
39- gap : 6 ,
59+ const Extras = styled . div ( {
60+ marginBottom : 2 ,
61+ } ) ;
62+
63+ const Checkbox = styled . input ( {
64+ margin : 0 ,
65+ '&:enabled' : {
66+ cursor : 'pointer' ,
67+ } ,
4068} ) ;
4169
4270export const TestProviderRender : FC < {
4371 api : API ;
4472 state : TestProviderConfig & TestProviderState < Details , Config > ;
4573} > = ( { state, api } ) => {
74+ const [ isEditing , setIsEditing ] = useState ( false ) ;
4675 const [ isModalOpen , setIsModalOpen ] = useState ( false ) ;
76+ const theme = useTheme ( ) ;
4777
48- const title = state . crashed || state . failed ? 'Component tests failed' : 'Component tests' ;
78+ const title = state . crashed || state . failed ? 'Local tests failed' : 'Run local tests' ;
4979 const errorMessage = state . error ?. message ;
5080
51- const [ config , changeConfig ] = useConfig (
81+ const [ config , updateConfig ] = useConfig (
82+ api ,
5283 state . id ,
53- state . config || { a11y : false , coverage : false } ,
54- api
84+ state . config || { a11y : false , coverage : false }
5585 ) ;
5686
57- const [ isEditing , setIsEditing ] = useState ( false ) ;
58-
5987 return (
60- < Fragment >
61- < Head >
88+ < Container >
89+ < Heading >
6290 < Info >
6391 < Title crashed = { state . crashed } id = "testing-module-title" >
6492 { title }
@@ -68,11 +96,11 @@ export const TestProviderRender: FC<{
6896
6997 < Actions >
7098 < Button
71- aria-label = { `Edit ` }
99+ aria-label = { `${ isEditing ? 'Close' : 'Open' } settings for ${ state . name } ` }
72100 variant = "ghost"
73101 padding = "small"
74102 active = { isEditing }
75- onClick = { ( ) => setIsEditing ( ( v ) => ! v ) }
103+ onClick = { ( ) => setIsEditing ( ! isEditing ) }
76104 >
77105 < EditIcon />
78106 </ Button >
@@ -105,7 +133,7 @@ export const TestProviderRender: FC<{
105133 aria-label = { `Start ${ state . name } ` }
106134 variant = "ghost"
107135 padding = "small"
108- onClick = { ( ) => api . runTestProvider ( state . id , { } ) }
136+ onClick = { ( ) => api . runTestProvider ( state . id ) }
109137 disabled = { state . crashed || state . running }
110138 >
111139 < PlayHollowIcon />
@@ -114,29 +142,60 @@ export const TestProviderRender: FC<{
114142 </ >
115143 ) }
116144 </ Actions >
117- </ Head >
118-
119- { ! isEditing ? (
120- < Fragment >
121- { Object . entries ( config ) . map ( ( [ key , value ] ) => (
122- < div key = { key } >
123- { key } : { value ? 'ON' : 'OFF' }
124- </ div >
125- ) ) }
126- </ Fragment >
145+ </ Heading >
146+
147+ { isEditing ? (
148+ < Extras >
149+ < ListItem
150+ as = "label"
151+ title = "Component tests"
152+ icon = { < PointerHandIcon color = { theme . textMutedColor } /> }
153+ right = { < Checkbox type = "checkbox" checked disabled /> }
154+ />
155+ < ListItem
156+ as = "label"
157+ title = "Coverage"
158+ icon = { < ShieldIcon color = { theme . textMutedColor } /> }
159+ right = {
160+ < Checkbox
161+ type = "checkbox"
162+ disabled // TODO: Implement coverage
163+ checked = { config . coverage }
164+ onChange = { ( ) => updateConfig ( { coverage : ! config . coverage } ) }
165+ />
166+ }
167+ />
168+ < ListItem
169+ as = "label"
170+ title = "Accessibility"
171+ icon = { < AccessibilityIcon color = { theme . textMutedColor } /> }
172+ right = {
173+ < Checkbox
174+ type = "checkbox"
175+ disabled // TODO: Implement a11y
176+ checked = { config . a11y }
177+ onChange = { ( ) => updateConfig ( { a11y : ! config . a11y } ) }
178+ />
179+ }
180+ />
181+ </ Extras >
127182 ) : (
128- < Fragment >
129- { Object . entries ( config ) . map ( ( [ key , value ] ) => (
130- < div
131- key = { key }
132- onClick = { ( ) => {
133- changeConfig ( { [ key ] : ! value } ) ;
134- } }
135- >
136- { key } : { value ? 'ON' : 'OFF' }
137- </ div >
138- ) ) }
139- </ Fragment >
183+ < Extras >
184+ < ListItem
185+ title = "Component tests"
186+ icon = { < TestStatusIcon status = "positive" aria-label = "status: passed" /> }
187+ />
188+ < ListItem
189+ title = "Coverage"
190+ icon = { < TestStatusIcon percentage = { 60 } status = "warning" aria-label = "status: warning" /> }
191+ right = { `60%` }
192+ />
193+ < ListItem
194+ title = "Accessibility"
195+ icon = { < TestStatusIcon status = "negative" aria-label = "status: failed" /> }
196+ right = { 73 }
197+ />
198+ </ Extras >
140199 ) }
141200
142201 < GlobalErrorModal
@@ -150,33 +209,35 @@ export const TestProviderRender: FC<{
150209 api . runTestProvider ( TEST_PROVIDER_ID ) ;
151210 } }
152211 />
153- </ Fragment >
212+ </ Container >
154213 ) ;
155214} ;
156215
157- function useConfig ( id : string , config : Config , api : API ) {
158- const data = useRef < Config > ( config ) ;
159- data . current = config || {
160- a11y : false ,
161- coverage : false ,
162- } ;
216+ function useConfig ( api : API , providerId : string , initialConfig : Config ) {
217+ const [ currentConfig , setConfig ] = useState < Config > ( initialConfig ) ;
218+ const lastConfig = useRef ( initialConfig ) ;
219+
220+ const saveConfig = useCallback (
221+ debounce ( ( config : Config ) => {
222+ if ( ! isEqual ( config , lastConfig . current ) ) {
223+ api . updateTestProviderState ( providerId , { config } ) ;
224+ api . emit ( TESTING_MODULE_CONFIG_CHANGE , { providerId, config } ) ;
225+ lastConfig . current = config ;
226+ }
227+ } , 500 ) ,
228+ [ api , providerId ]
229+ ) ;
163230
164- const changeConfig = useCallback (
231+ const updateConfig = useCallback (
165232 ( update : Partial < Config > ) => {
166- const newConfig = {
167- ...data . current ,
168- ...update ,
169- } ;
170- api . updateTestProviderState ( id , {
171- config : newConfig ,
233+ setConfig ( ( value ) => {
234+ const updated = { ...value , ...update } ;
235+ saveConfig ( updated ) ;
236+ return updated ;
172237 } ) ;
173- api . emit ( TESTING_MODULE_CONFIG_CHANGE , {
174- providerId : id ,
175- config : newConfig ,
176- } as TestingModuleConfigChangePayload ) ;
177238 } ,
178- [ api , id ]
239+ [ saveConfig ]
179240 ) ;
180241
181- return [ data . current , changeConfig ] as const ;
242+ return [ currentConfig , updateConfig ] as const ;
182243}
0 commit comments