@@ -345,14 +345,15 @@ describe("<QuizQuestion />", () => {
345345 expect ( actionButton1 ) . toBeInTheDocument ( ) ;
346346 expect ( actionButton2 ) . toBeInTheDocument ( ) ;
347347
348- expect ( actionButton1 ) . toHaveAttribute (
349- "aria-describedby" ,
350- "quiz-answer-1-label" ,
351- ) ;
352- expect ( actionButton2 ) . toHaveAttribute (
353- "aria-describedby" ,
354- "quiz-answer-2-label" ,
355- ) ;
348+ const label1Id = actionButton1 . getAttribute ( "aria-describedby" ) ;
349+ const label2Id = actionButton2 . getAttribute ( "aria-describedby" ) ;
350+
351+ expect ( label1Id ) . toBeTruthy ( ) ;
352+ expect ( label2Id ) . toBeTruthy ( ) ;
353+ expect ( label1Id ) . not . toBe ( label2Id ) ;
354+
355+ expect ( screen . getByText ( "Option 1" ) . id ) . toBe ( label1Id ) ;
356+ expect ( screen . getByText ( "Option 2" ) . id ) . toBe ( label2Id ) ;
356357
357358 await userEvent . click ( actionButton1 ) ;
358359 expect ( handleAction1 ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -368,6 +369,58 @@ describe("<QuizQuestion />", () => {
368369 ) . not . toBeInTheDocument ( ) ;
369370 } ) ;
370371
372+ it ( "should have unique action button aria-describedby IDs across multiple QuizQuestion instances with the same answer values" , ( ) => {
373+ const answers = [
374+ {
375+ label : "Option 1" ,
376+ value : 1 ,
377+ action : { onClick : vi . fn ( ) , ariaLabel : "Speak option 1" } ,
378+ } ,
379+ {
380+ label : "Option 2" ,
381+ value : 2 ,
382+ action : { onClick : vi . fn ( ) , ariaLabel : "Speak option 2" } ,
383+ } ,
384+ ] ;
385+
386+ render (
387+ < >
388+ < QuizQuestion question = "Question 1" answers = { answers } />
389+ < QuizQuestion question = "Question 2" answers = { answers } />
390+ </ > ,
391+ ) ;
392+
393+ const question1 = screen . getByRole ( "radiogroup" , { name : "Question 1" } ) ;
394+ const question2 = screen . getByRole ( "radiogroup" , { name : "Question 2" } ) ;
395+
396+ const speak1A = within ( question1 ) . getByRole ( "button" , {
397+ name : "Speak option 1" ,
398+ } ) ;
399+ const speak1B = within ( question1 ) . getByRole ( "button" , {
400+ name : "Speak option 2" ,
401+ } ) ;
402+ const speak2A = within ( question2 ) . getByRole ( "button" , {
403+ name : "Speak option 1" ,
404+ } ) ;
405+ const speak2B = within ( question2 ) . getByRole ( "button" , {
406+ name : "Speak option 2" ,
407+ } ) ;
408+
409+ const id1A = speak1A . getAttribute ( "aria-describedby" ) ! ;
410+ const id1B = speak1B . getAttribute ( "aria-describedby" ) ! ;
411+ const id2A = speak2A . getAttribute ( "aria-describedby" ) ! ;
412+ const id2B = speak2B . getAttribute ( "aria-describedby" ) ! ;
413+
414+ // All four IDs must be distinct
415+ expect ( new Set ( [ id1A , id1B , id2A , id2B ] ) . size ) . toBe ( 4 ) ;
416+
417+ // Each button must point to the correct label within its question
418+ expect ( within ( question1 ) . getByText ( "Option 1" ) . id ) . toBe ( id1A ) ;
419+ expect ( within ( question1 ) . getByText ( "Option 2" ) . id ) . toBe ( id1B ) ;
420+ expect ( within ( question2 ) . getByText ( "Option 1" ) . id ) . toBe ( id2A ) ;
421+ expect ( within ( question2 ) . getByText ( "Option 2" ) . id ) . toBe ( id2B ) ;
422+ } ) ;
423+
371424 it ( "should not render action buttons when not provided" , ( ) => {
372425 render (
373426 < QuizQuestion
0 commit comments