@@ -16,14 +16,16 @@ import {
1616 Play ,
1717 Share2 ,
1818 Tag ,
19+ Wrench ,
1920} from 'lucide-react' ;
2021import { Badge } from '@/components/ui/badge' ;
2122import { Button } from '@/components/ui/button' ;
2223import { Card , CardContent , CardHeader , CardTitle } from '@/components/ui/card' ;
2324import { Switch } from '@/components/ui/switch' ;
2425import { useTranslation } from '@/hooks/useTranslation' ;
2526import { useToast } from '@/hooks/use-toast' ;
26- import { api , customFieldsAPI , datasetsAPI , sectionsAPI , testCasesAPI , testSuitesAPI , type TestDataset } from '@/lib/api' ;
27+ import { api , customFieldsAPI , datasetsAPI , sectionsAPI , testCasesAPI , testSuitesAPI , type TestDataset , type GlobalParameter } from '@/lib/api' ;
28+ import { loadProjectParameters , paramsToMap , referencedKeys , resolveParameters } from '@/utils/parameters' ;
2729import { CustomFieldDefinition , CustomFieldValue , Requirement , TestCase , TestSuite } from '@/types' ;
2830
2931type SectionCrumb = { id : number ; name : string } ;
@@ -70,6 +72,7 @@ export function TestCaseDetail() {
7072 const navigate = useNavigate ( ) ;
7173 const [ testCase , setTestCase ] = useState < TestCase | null > ( null ) ;
7274 const [ dataset , setDataset ] = useState < TestDataset | null > ( null ) ;
75+ const [ globalParams , setGlobalParams ] = useState < GlobalParameter [ ] > ( [ ] ) ;
7376 const [ testSuite , setTestSuite ] = useState < TestSuite | null > ( null ) ;
7477 const [ section , setSection ] = useState < { name : string ; path : SectionCrumb [ ] } | null > ( null ) ;
7578 const [ testSteps , setTestSteps ] = useState < Array < {
@@ -334,6 +337,21 @@ export function TestCaseDetail() {
334337 return ( ) => { cancelled = true ; } ;
335338 } , [ attachedDatasetId ] ) ;
336339
340+ // Load the project's global parameters so we can show which ones this case
341+ // references via ${name} and what they resolve to.
342+ useEffect ( ( ) => {
343+ const numericProjectId = Number ( effectiveProjectId ) ;
344+ if ( ! Number . isFinite ( numericProjectId ) ) {
345+ setGlobalParams ( [ ] ) ;
346+ return ;
347+ }
348+ let cancelled = false ;
349+ loadProjectParameters ( numericProjectId )
350+ . then ( ( rows ) => { if ( ! cancelled ) setGlobalParams ( rows ) ; } )
351+ . catch ( ( ) => { if ( ! cancelled ) setGlobalParams ( [ ] ) ; } ) ;
352+ return ( ) => { cancelled = true ; } ;
353+ } , [ effectiveProjectId ] ) ;
354+
337355 const displaySteps = useMemo ( ( ) => {
338356 const parseLegacyText = ( text : string | undefined | null ) =>
339357 ( parseCodeFence ( text || '' ) ?. code ?? ( text || '' ) )
@@ -362,6 +380,24 @@ export function TestCaseDetail() {
362380 return parsed ;
363381 } , [ isMultistepCase , testCase , testSteps ] ) ;
364382
383+ // Global parameters actually referenced by this case's text, paired with the
384+ // value they resolve to during a run.
385+ const referencedParams = useMemo ( ( ) => {
386+ if ( globalParams . length === 0 ) return [ ] ;
387+ const text = [
388+ testCase ?. preconditions ,
389+ testCase ?. steps ,
390+ testCase ?. expected_result ,
391+ ...displaySteps . flatMap ( ( s ) => [ s . action , s . expected_result ] ) ,
392+ ] . filter ( Boolean ) . join ( '\n' ) ;
393+ const keys = new Set ( referencedKeys ( text ) ) ;
394+ return globalParams . filter ( ( p ) => keys . has ( p . name ) ) ;
395+ } , [ globalParams , testCase , displaySteps ] ) ;
396+
397+ // Resolve ${name} placeholders to their global-parameter values for display.
398+ const globalMap = useMemo ( ( ) => paramsToMap ( globalParams ) , [ globalParams ] ) ;
399+ const resolve = ( text : string | null | undefined ) : string => resolveParameters ( text , globalMap ) ;
400+
365401 const latestExecution = testRunHistory [ 0 ] ;
366402 const uniqueRunCount = new Set (
367403 testRunHistory . map ( ( item ) => item . test_run_id ) . filter ( ( value ) => value != null )
@@ -614,7 +650,7 @@ export function TestCaseDetail() {
614650 < CardContent className = "pt-0" >
615651 < div className = "rounded-2xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/60" >
616652 < p className = "whitespace-pre-wrap wrap-break-word text-[15px] leading-7 text-slate-700 dark:text-slate-300" >
617- { testCase . preconditions || t ( 'noPreconditions' ) }
653+ { testCase . preconditions ? resolve ( testCase . preconditions ) : t ( 'noPreconditions' ) }
618654 </ p >
619655 </ div >
620656 </ CardContent >
@@ -663,6 +699,42 @@ export function TestCaseDetail() {
663699 </ Card >
664700 ) }
665701
702+ { referencedParams . length > 0 && (
703+ < Card className = "border-slate-200 bg-white shadow-xs dark:border-slate-800 dark:bg-slate-900" >
704+ < CardHeader className = "pb-3" >
705+ < CardTitle className = "flex flex-wrap items-center gap-2 text-base font-semibold text-slate-950 dark:text-white" >
706+ < span className = "rounded-lg bg-amber-100 p-1.5 dark:bg-amber-900/30" >
707+ < Wrench className = "h-4 w-4 text-amber-600 dark:text-amber-300" />
708+ </ span >
709+ { t ( 'globalParameters' ) }
710+ </ CardTitle >
711+ </ CardHeader >
712+ < CardContent className = "pt-0 space-y-2" >
713+ < p className = "text-xs text-slate-500 dark:text-slate-400" > { t ( 'globalParamsReferencedHint' ) } </ p >
714+ < div className = "overflow-x-auto rounded-2xl border border-slate-200 dark:border-slate-800" >
715+ < table className = "w-full text-sm" >
716+ < thead >
717+ < tr className = "bg-slate-50 dark:bg-slate-950/60" >
718+ < th className = "px-3 py-2 text-left font-mono text-xs text-slate-700 dark:text-slate-300" > { t ( 'name' ) } </ th >
719+ < th className = "px-3 py-2 text-left text-xs text-slate-500" > { t ( 'value' ) } </ th >
720+ </ tr >
721+ </ thead >
722+ < tbody >
723+ { referencedParams . map ( ( p ) => (
724+ < tr key = { p . id } className = "border-t border-slate-200 dark:border-slate-800" >
725+ < td className = "px-3 py-2 font-mono text-xs text-slate-700 dark:text-slate-300" > { `\${${ p . name } }` } </ td >
726+ < td className = "px-3 py-2 text-slate-700 dark:text-slate-300" >
727+ { p . is_encrypted ? < span className = "text-slate-400" > ••••••</ span > : p . value }
728+ </ td >
729+ </ tr >
730+ ) ) }
731+ </ tbody >
732+ </ table >
733+ </ div >
734+ </ CardContent >
735+ </ Card >
736+ ) }
737+
666738 < Card className = "border-slate-200 bg-white shadow-xs dark:border-slate-800 dark:bg-slate-900" >
667739 < CardHeader className = "pb-3" >
668740 < CardTitle className = "flex flex-wrap items-center gap-2 text-base font-semibold text-slate-950 dark:text-white" >
@@ -699,13 +771,13 @@ export function TestCaseDetail() {
699771 < div >
700772 < h5 className = "mb-1 text-sm font-medium text-slate-700 dark:text-slate-300" > { t ( 'action' ) } </ h5 >
701773 < p className = "whitespace-pre-wrap wrap-break-word text-sm leading-7 text-slate-600 dark:text-slate-300" >
702- { step . action || t ( 'noStepsDefined' ) }
774+ { step . action ? resolve ( step . action ) : t ( 'noStepsDefined' ) }
703775 </ p >
704776 </ div >
705777 < div >
706778 < h5 className = "mb-1 text-sm font-medium text-slate-700 dark:text-slate-300" > { t ( 'expectedResult' ) } </ h5 >
707779 < p className = "whitespace-pre-wrap wrap-break-word text-sm leading-7 text-slate-600 dark:text-slate-300" >
708- { step . expected_result || t ( 'noExpectedResults' ) }
780+ { step . expected_result ? resolve ( step . expected_result ) : t ( 'noExpectedResults' ) }
709781 </ p >
710782 </ div >
711783 </ div >
@@ -720,7 +792,7 @@ export function TestCaseDetail() {
720792 )
721793 ) : testCase . steps ? (
722794 < div className = "rounded-2xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/60" >
723- < StepsTextContent value = { testCase . steps } />
795+ < StepsTextContent value = { resolve ( testCase . steps ) } />
724796 </ div >
725797 ) : (
726798 < div className = "rounded-2xl border border-dashed border-slate-300 bg-slate-50/80 p-8 text-center text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-950/60" >
@@ -745,7 +817,7 @@ export function TestCaseDetail() {
745817 { testCase . expected_result ? (
746818 < div className = "rounded-2xl border border-slate-200 bg-slate-50/80 p-4 dark:border-slate-800 dark:bg-slate-950/60" >
747819 < p className = "whitespace-pre-wrap wrap-break-word text-sm leading-7 text-slate-700 dark:text-slate-300" >
748- { testCase . expected_result }
820+ { resolve ( testCase . expected_result ) }
749821 </ p >
750822 </ div >
751823 ) : (
0 commit comments