Testcraft Studio frontend 32: src/pages/Suites.jsx
importReact,{useEffect,useState}from'react';importIconfrom'../Icon.jsx';import{defaultFields}from'../seed.js';import{useStudio,canReview,download,log}from'../store.jsx';import{suiteExport}from'../suiteExport.js';import{Badge,Button,ContextBar,Empty,Field,Heading,Modal,Note,Search,SourceDialog,Tabs,WorkspaceRequired}from'../ui.jsx';exportdefaultfunctionSuites({navigate,initialSuiteId=''}){const{state:s,workspace:w,mutate,notify}=useStudio();const[suiteId,setSuite]=useState(initialSuiteId),[testId,setTest]=useState(''),[query,setQuery]=useState(''),[filter,setFilter]=useState('All'),[tab,setTab]=useState('Scenarios'),[editing,setEditing]=useState(null),[source,setSource]=useState('');useEffect(()=>{setSuite(initialSuiteId);setTest('');},[initialSuiteId]);if (!w)return<WorkspaceRequirednavigate={navigate}/>;constlist=s.suites.filter(t=>t.workspaceId===w.id),suite=list.find(t=>t.id===suiteId)||list[0];consttests=(suite?.tests||[]).filter(t=>`${t.id}${t.title}${t.requirement}`.toLowerCase().includes(query.toLowerCase())&&(filter==='All'||t.status===filter));constselected=suite?.tests.find(t=>t.id===testId)||tests[0];consttemplate=suite?.templateSnapshot||s.templates.find(t=>t.id===suite?.templateId);constcitationFor=id=>suite?.sourceSnapshot?.find(item=>item.id===id)||s.documents.find(item=>item.id===id);constextraFields=(template?.fields||[]).filter(field=>!defaultFields.some(known=>known.toLowerCase()===field.toLowerCase()));constincludesField=name=>!template||template.fields.some(field=>field.toLowerCase()===name.toLowerCase());functiondecide(status){if (!selected||!canReview(s))return;mutate(d=>{constt=d.suites.find(t=>t.id===suite.id);consttest=t.tests.find(test=>test.id===selected.id);test.status=status;test.reviewedBy=s.session.name;test.revision=(test.revision||1)+1;t.status=t.provisional?'PROVISIONAL':t.tests.every(test=>test.status==='Accepted')?'Accepted':'Ready for review';log(d,`${status}: ${selected.id}`);});notify(`Scenario ${status.toLowerCase()}.`);}functionexportSuite(){constoutput=suiteExport(s,w,suite);download(output.filename,output.content,output.mimeType);notify('Test cases exported in the selected template column order.');}return<><Headingeyebrow={`${w.ref} · REVIEW WITH TRACEABILITY`}title="Test suites"description="Review the scenario, its expected behavior, and the evidence behind it."><divclassName="row-actions"><ButtononClick={()=>navigate('readiness')}>New generation</Button><Buttonvariant="primary"icon="external"disabled={!suite}onClick={exportSuite}>Export CSV</Button></div></Heading><ContextBarnavigate={navigate}/>{!suite?<sectionclassName="card margin-top"><Emptytitle="No test suites yet"description="Review readiness and generate a test suite for this workspace."action={<Buttonvariant="primary"onClick={()=>navigate('readiness')}>Review readiness</Button>}/></section>:<><divclassName="suite-toolbar"><selectclassName="wide-select"value={suite.id}onChange={e=>{setSuite(e.target.value);setTest('');}}>{list.map(t=><optionkey={t.id}value={t.id}>{t.name} · Context {t.version}</option>)}</select><Badge>{suite.status}</Badge><spanclassName="subtle">{template?.name} · v{template?.version}</span></div>{suite.version!==w.version&&<Notetone="amber">This historic suite is pinned to context {suite.version}. The current workspace uses context {w.version}. Reviewing it does not change its knowledge version.</Note>}{suite.provisional&&<Notetone="amber">PROVISIONAL · {suite.gaps.length} unresolved requirements. Unsupported expected outcomes remain unresolved. This suite does not establish complete coverage.</Note>}<Tabsvalue={tab}onChange={setTab}items={['Scenarios','Coverage & gaps']}/>{tab==='Coverage & gaps'?<sectionclassName="card"><divclassName="section-heading"><h2>Concrete coverage and review status</h2><spanclassName="count-pill">{suite.tests.length} tests</span></div><divclassName="readiness-tiles padded"><div><strong>{newSet(suite.tests.map(t=>t.requirement)).size}</strong><span>Requirements represented</span></div><div><strong>{suite.tests.filter(t=>!t.sourceIds.length&&!t.answerEvidence).length}</strong><span>Missing citations</span></div><div><strong>{suite.tests.filter(t=>t.expected==='Expected result unresolved').length}</strong><span>Unsupported outcomes</span></div><div><strong>{suite.tests.filter(t=>t.status==='Accepted').length}</strong><span>Accepted tests</span></div></div><divclassName="table-scroll"><table><thead><tr><th>REQUIREMENT</th><th>TEST CASES</th><th>REVIEW</th></tr></thead><tbody>{[...newSet(suite.tests.map(t=>t.requirement))].map(id=><trkey={id}><td>{id}</td><td>{suite.tests.filter(t=>t.requirement===id).map(t=>t.id).join(', ')}</td><td><Badgetone={suite.tests.some(t=>t.requirement===id&&t.expected==='Expected result unresolved')?'amber':'green'}>{suite.tests.some(t=>t.requirement===id&&t.expected==='Expected result unresolved')?'Evidence gap':'Source linked'}</Badge></td></tr>)}</tbody></table></div><divclassName="padded"><Note>These measures describe this suite only. They do not claim full requirement coverage or model accuracy.</Note></div></section>:<divclassName="suite-grid"><sectionclassName="card scenario-list"><divclassName="padded"><Searchvalue={query}onChange={setQuery}placeholder="Search scenarios…"/><selectclassName="wide-select margin-top"value={filter}onChange={e=>setFilter(e.target.value)}>{['All','Generated','Accepted','Revision requested','Rejected'].map(v=><optionkey={v}>{v}</option>)}</select></div>{tests.map(t=><buttonclassName={`scenario-item ${selected?.id===t.id?'selected':''}`}key={t.id}onClick={()=>setTest(t.id)}><div><span>{t.id}</span><Badge>{t.status}</Badge></div><strong>{t.title}</strong><small>{t.category} · {t.requirement}</small></button>)}{!tests.length&&<Emptytitle="No matching scenarios"description="Clear your search or status filter."/>}</section>{selected&&<sectionclassName="card padded scenario-detail"><divclassName="section-heading inset"><spanclassName="code-label">{selected.id}</span><Buttonicon="file"onClick={()=>setEditing({...selected,steps:selected.steps.join('\n')})}>Edit scenario</Button></div><h2className="scenario-title">{selected.title}</h2><divclassName="detail-summary"><Badgetone="purple">{selected.category}</Badge><Badge>{selected.status}</Badge><span>{selected.requirement} · Revision {selected.revision||1}</span></div>{[['Objective',selected.objective],['Preconditions',selected.preconditions],['Test data',selected.data]].filter(([label])=>includesField(label)).map(([label,value])=><divclassName="test-field"key={label}><h3>{label}</h3><p>{value||'Not specified'}</p></div>)}<divclassName="test-field"><h3>Steps</h3><olclassName="test-steps">{selected.steps.map((step,i)=><likey={i}><span>{i+1}</span>{step}</li>)}</ol></div><divclassName="test-field"><h3>Expected result</h3><divclassName={`expected-result ${selected.expected==='Expected result unresolved'?'unresolved':''}`}>{selected.expected}</div></div><divclassName="test-field"><h3>Priority · {selected.priority}</h3><p>{selected.rationale}</p></div>{extraFields.map(field=><divclassName="test-field"key={field}><h3>{field}</h3><p>{selected.customFields?.[field]||"Not provided"}</p></div>)}<divclassName="test-field"><h3>Evidence citations</h3>{selected.sourceIds.map(id=><buttonclassName="source-citation"key={id}onClick={()=>setSource(id)}><Iconname="file"size={19}/><span><strong>{citationFor(id)?.name||'Source unavailable'}</strong><small>{citationFor(id)?.locator} · Revision {citationFor(id)?.revision}</small></span><Iconname="external"size={14}/></button>)}{selected.answerEvidence&&<Note>User-provided reviewed evidence: {selected.answerEvidence}</Note>}</div><Fieldlabel="Review notes"><textareavalue={selected.notes||''}rows={2}onChange={e=>mutate(d=>{d.suites.find(t=>t.id===suite.id).tests.find(t=>t.id===selected.id).notes=e.target.value;})}placeholder="Document changes or team review notes…"/></Field><divclassName="review-actions"><Buttonvariant="primary"icon="check"disabled={!canReview(s)||selected.expected==='Expected result unresolved'}onClick={()=>decide('Accepted')}>Accept scenario</Button><Buttondisabled={!canReview(s)}onClick={()=>decide('Revision requested')}>Request revision</Button><Buttondisabled={!canReview(s)}onClick={()=>decide('Rejected')}>Reject</Button></div>{!canReview(s)&&<pclassName="supporting-copy">Sign in to review this scenario.</p>}</section>}</div>}</>}{editing&&<Modaltitle="Edit test scenario"close={()=>setEditing(null)}wide><formonSubmit={e=>{e.preventDefault();mutate(d=>{constt=d.suites.find(t=>t.id===suite.id).tests.find(t=>t.id===editing.id);Object.assign(t,editing,{steps:editing.steps.split('\n').map(step=>step.trim()).filter(Boolean),status:'Revision requested',revision:(t.revision||1)+1});log(d,`Edited ${editing.id}`);});setEditing(null);notify('Scenario revision saved. Team review acceptance reset.');}}><Fieldlabel="Title"><inputrequiredvalue={editing.title}onChange={e=>setEditing(t=>({...t,title:e.target.value}))}/></Field>{[['Objective','objective'],['Preconditions','preconditions'],['Test data','data'],['Steps (one per line)','steps'],['Expected result','expected']].map(([label,key])=><Fieldkey={key}label={label}><textarearequired={['steps','expected'].includes(key)}rows={key==='steps'?4:2}value={editing[key]}onChange={e=>setEditing(t=>({...t,[key]:e.target.value}))}/></Field>)}<Fieldlabel="Priority"><selectvalue={editing.priority}onChange={e=>setEditing(t=>({...t,priority:e.target.value}))}>{['High','Medium','Low','Unassigned'].map(v=><optionkey={v}>{v}</option>)}</select></Field>{extraFields.map(field=><Fieldkey={field}label={field}><inputvalue={editing.customFields?.[field]||""}onChange={e=>setEditing(t=>({...t,customFields:{...t.customFields,[field]:e.target.value}}))}/></Field>)}<Note>Edits are user-provided revisions. Check every changed expected result against the pinned evidence before accepting.</Note><divclassName="modal-actions"><ButtononClick={()=>setEditing(null)}>Cancel</Button><Buttontype="submit"variant="primary">Save revision</Button></div></form></Modal>}{source&&<SourceDialogsourceId={source}citation={suite?.sourceSnapshot?.find(item=>item.id===source)}close={()=>setSource('')}/>}</>;}
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)