Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/quiz-question/answer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface AnswerProps<
> extends QuizQuestionAnswer<AnswerT> {
checked?: boolean;
disabled?: boolean;
questionId: string;
}

const radioIconDefaultClasses = [
Expand Down Expand Up @@ -119,8 +120,9 @@ export const Answer = <AnswerT extends number | string>({
validation,
feedback,
action,
questionId,
}: AnswerProps<AnswerT>) => {
const labelId = `quiz-answer-${value}-label`;
const labelId = `quiz-answer-${questionId}-${value}-label`;

const getRadioWrapperCls = () => {
const cls = [...radioWrapperDefaultClasses];
Expand Down
2 changes: 2 additions & 0 deletions src/quiz-question/quiz-question.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const story = {
type Story = StoryObj<typeof QuizQuestion>;

const QuizQuestionComp = <AnswerT extends number | string>({
id = "question-0",
question,
answers = [],
disabled,
Expand All @@ -40,6 +41,7 @@ const QuizQuestionComp = <AnswerT extends number | string>({

return (
<QuizQuestion
id={id}
question={question}
answers={answers}
disabled={disabled}
Expand Down
27 changes: 25 additions & 2 deletions src/quiz-question/quiz-question.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("<QuizQuestion />", () => {
it("should render as a radio group", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand Down Expand Up @@ -43,6 +44,7 @@ describe("<QuizQuestion />", () => {
it("should have the `required` attribute set to `true` if the `required` prop is specified", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -61,6 +63,7 @@ describe("<QuizQuestion />", () => {
it("should have the `aria-disabled` attribute set to `true` if the `disabled` prop is specified", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand Down Expand Up @@ -88,6 +91,7 @@ describe("<QuizQuestion />", () => {
it("should have the correct `checked` attribute according to the `selectedOption` prop", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -108,6 +112,7 @@ describe("<QuizQuestion />", () => {

render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -127,6 +132,7 @@ describe("<QuizQuestion />", () => {
it("should render the correct state properly", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{
Expand Down Expand Up @@ -157,6 +163,7 @@ describe("<QuizQuestion />", () => {
it("should render the incorrect state properly", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{
Expand Down Expand Up @@ -187,6 +194,7 @@ describe("<QuizQuestion />", () => {
it("should render the question position properly", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -205,6 +213,7 @@ describe("<QuizQuestion />", () => {
it("should render answer feedback if both `feedback` and `validation` are provided", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{
Expand Down Expand Up @@ -238,6 +247,7 @@ describe("<QuizQuestion />", () => {
it("should not render answer feedback if `feedback` is provided but `validation` is not", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1, feedback: "Quis vel quo saepe." },
Expand All @@ -257,6 +267,7 @@ describe("<QuizQuestion />", () => {
it("should only render the feedback of the selected answer", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{
Expand Down Expand Up @@ -308,6 +319,7 @@ describe("<QuizQuestion />", () => {

render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{
Expand Down Expand Up @@ -345,13 +357,17 @@ describe("<QuizQuestion />", () => {
expect(actionButton1).toBeInTheDocument();
expect(actionButton2).toBeInTheDocument();

const label1 = screen.getByText("Option 1");
const label2 = screen.getByText("Option 2");
expect(label1).toHaveAttribute("id", "quiz-answer-question-0-1-label");
expect(label2).toHaveAttribute("id", "quiz-answer-question-0-2-label");
expect(actionButton1).toHaveAttribute(
"aria-describedby",
"quiz-answer-1-label",
"quiz-answer-question-0-1-label",
);
expect(actionButton2).toHaveAttribute(
"aria-describedby",
"quiz-answer-2-label",
"quiz-answer-question-0-2-label",
);

await userEvent.click(actionButton1);
Expand All @@ -371,6 +387,7 @@ describe("<QuizQuestion />", () => {
it("should not render action buttons when not provided", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -388,6 +405,7 @@ describe("<QuizQuestion />", () => {
it("should render audio and transcript when audioUrl is provided", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -412,6 +430,7 @@ describe("<QuizQuestion />", () => {
it("should render audio with correct aria-label when position is provided", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -433,6 +452,7 @@ describe("<QuizQuestion />", () => {
it("should not render audio or transcript when audioUrl is not provided", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -451,6 +471,7 @@ describe("<QuizQuestion />", () => {
it("should pass audioStartTime and audioFinishTime to Audio component", () => {
render(
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand Down Expand Up @@ -581,6 +602,7 @@ describe("<QuizQuestion />", () => {

// QuizQuestion with valid audio props
<QuizQuestion
id="question-0"
question="Lorem ipsum"
answers={[
{ label: "Option 1", value: 1 },
Expand All @@ -594,6 +616,7 @@ describe("<QuizQuestion />", () => {
// Type tests for audio segment props
// Should allow audioStartTime and audioFinishTime only when audioUrl is present
<QuizQuestion
id="question-0"
question="Audio segment question"
answers={[{ label: "Option 1", value: 1 }]}
audioUrl="test-audio.mp3"
Expand Down
2 changes: 2 additions & 0 deletions src/quiz-question/quiz-question.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const QuestionText = ({
* giving the parent component full control over the selection handling logic.
*/
export const QuizQuestion = <AnswerT extends number | string>({
id: questionId,
question,
answers,
required,
Expand Down Expand Up @@ -98,6 +99,7 @@ export const QuizQuestion = <AnswerT extends number | string>({
disabled={disabled}
validation={validation}
action={action}
questionId={questionId}
/>
);
})}
Expand Down
5 changes: 5 additions & 0 deletions src/quiz-question/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface QuizQuestionValidation {
}

interface QuizQuestionBaseProps<AnswerT extends number | string> {
/**
* Unique identifier for the question, used to generate accessible IDs for answers
*/
id: string;

/**
* Question text, can be plain text or contain code.
* If the question text contains code, use the PrismFormatted component to ensure the code is rendered correctly.
Expand Down
17 changes: 17 additions & 0 deletions src/quiz/quiz.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ describe("<Quiz />", () => {
});
});

it("should render answer labels with unique ids across questions", () => {
render(<ControlledQuiz />);

const question1 = screen.getByRole("radiogroup", {
name: "1. Lorem ipsum dolor sit amet",
});
const question2 = screen.getByRole("radiogroup", {
name: "2. Consectetur adipiscing elit",
});

const q1Label = within(question1).getByText("Option 1");
const q2Label = within(question2).getByText("Option 1");

expect(q1Label).toHaveAttribute("id", "quiz-answer-question-0-1-label");
expect(q2Label).toHaveAttribute("id", "quiz-answer-question-1-1-label");
});

it("should render action buttons when answers have action configuration", async () => {
const actionHandlers = {
q1a1: vi.fn(),
Expand Down
1 change: 1 addition & 0 deletions src/quiz/quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Quiz = <AnswerT extends number | string>({
<li key={index}>
<QuizQuestion
{...question}
id={`question-${index}`}
position={index + 1}
disabled={disabled}
required={required}
Expand Down
Loading