Skip to content

Test pages types overhaul#2209

Open
jacbn wants to merge 11 commits into
mainfrom
feature/test-pages-overhaul
Open

Test pages types overhaul#2209
jacbn wants to merge 11 commits into
mainfrom
feature/test-pages-overhaul

Conversation

@jacbn

@jacbn jacbn commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Overhauls the test pages' types to drastically increase code readability and robustness.

Quizzes across these pages can either be simplified rubric summary objects (if you have not yet committed to starting the test), or full quiz objects (if viewing a past attempt, previewing, or coming from an assignment). Previously, we represented the difference with two entirely separate objects:

// summary object
interface QuizViewProps extends QuizProps {
    attempt?: undefined;
    view: QuizView;
    preview?: undefined;
    page?: undefined;
    pageLink?: undefined;
    questions?: undefined;
    sections?: undefined;
}

// full quiz
interface QuizAttemptProps extends QuizProps {
    attempt: QuizAttemptDTO
    view?: undefined;
    preview?: boolean;
    page: number | null;
    pageLink: PageLinkCreator;
    questions: QuestionDTO[];
    sections: { [id: string]: IsaacQuizSectionDTO };
}

// (along with these props, shared between the two)
interface QuizProps {
    user: RegisteredUserDTO;
    pageHelp: React.ReactElement;
    studentUser?: UserSummaryDTO;
    quizAssignmentId?: string;
}

This is fairly nasty for multiple reasons:

  • All of these properties exist (undefined or not) across all quiz-related components, requiring conditional if-exists checks that would already be answered if we knew the type in advance;
  • The rubric exists in entirely different places across the two interfaces: QuizViewProps.view contains a DetailedQuizSummaryDTO (containing the rubric), whereas QuizAttemptProps.attempt contains an IsaacQuizDTO with this same information.
    • Indeed, DetailedQuizSummaryDTO is a strict subset of IsaacQuizDTO, which implies a significantly neater strategy of sharing an object between the two.

A majority of the changes in this PR migrate the above types into this new format:

type QuizContents = {
    questions: QuestionDTO[];
    sections: { [id: string]: IsaacQuizSectionDTO };
    pageLink: (page?: number) => string;
};

export type FullQuizInfo = {
    quiz: IsaacQuizDTO;
    attempt: QuizAttemptDTO;
    quizContents: QuizContents;
};

export type QuizSummaryInfo = {
    quiz: DetailedQuizSummaryDTO;
}

export type QuizProps = {
    user: RegisteredUserDTO;
    pageHelp: React.ReactElement;
    studentUser?: UserSummaryDTO;
    quizAssignmentId?: string;
    page?: number;
} & (FullQuizInfo | QuizSummaryInfo);

We can use the type QuizProps & FullQuizInfo whenever we require parameters to be those of a complete quiz, and similarly for QuizProps & QuizSummaryInfo if only the summary is required. We can also use QuizProps without a second modifier in the case we can use either; in particular, note that QuizProps.quiz is of type IsaacQuizDTO | DetailedQuizSummaryDTO, for which quiz.rubric is a valid property in both cases, and so never requires further type checking or complication.

Comment thread src/app/components/elements/cards/GameboardCard.tsx Fixed
@jsharkey13 jsharkey13 force-pushed the feature/test-pages-overhaul branch 2 times, most recently from b20310d to c504e1d Compare June 15, 2026 08:54
@codecov

codecov Bot commented Jun 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.89655% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 43.54%. Comparing base (bc6477b) to head (efeed83).
⚠️ Report is 94 commits behind head on main.

Files with missing lines Patch % Lines
...c/app/components/elements/list-groups/ListView.tsx 10.00% 9 Missing ⚠️
...p/components/pages/quizzes/QuizAttemptFeedback.tsx 0.00% 6 Missing ⚠️
.../app/components/pages/quizzes/QuizDoAssignment.tsx 25.00% 3 Missing ⚠️
...ents/elements/list-groups/AbstractListViewItem.tsx 66.66% 1 Missing ⚠️
...rc/app/components/elements/sidebar/QuizSidebar.tsx 90.00% 1 Missing ⚠️
...c/app/components/pages/quizzes/PracticeQuizzes.tsx 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2209      +/-   ##
==========================================
- Coverage   43.54%   43.54%   -0.01%     
==========================================
  Files         597      597              
  Lines       25217    25274      +57     
  Branches     8378     8424      +46     
==========================================
+ Hits        10981    11005      +24     
- Misses      14187    14212      +25     
- Partials       49       57       +8     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jacbn jacbn marked this pull request as ready for review June 15, 2026 13:06
@barna-isaac

barna-isaac commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

I agree the refactor described above is an improvement, thank you for doing it.

Besides the refactor, I also see the following user-facing changes:

  • the PR introduces a new version of the "Practice tests" page just for teachers. Instead of taking teachers to the /view page, where they could choose to preview, attempt or set the test, teachers are immediately taken to the /preview page, where they can preview or set the test. This makes it more difficult for a teacher to attempt a test. As far as I can see, if a teacher wants to try taking a test, they now need to assign the test to themselves first, or rewrite the url.
  • This also means the /view page is now for students only (as in only students get a link to it), and I assume this is why the teacher-only "Set test" button has been removed from it.
  • The /view page now shows a "You're freely attempting this test" message. I wonder if this might misleadingly suggest to students that an attempt on the test has already started, as the attempt only starts after the student has clicked the "Take test" button on this page. Just an observation, not something we need to fix immediately.

There are a lot of subtle changes and it's difficult to see all implications, so there might be a few changes I've missed. At any rate, the core functionality around quizzes appears to work well enough. I've verified manually that tests can be assigned and completed, that results are correctly recorded, that students can see feedback (when this is set), and that teachers can see test results on the markbook.

I've left a few comments with suggestions for simplifications, and flagged one logic and one rendering issue.

@barna-isaac

barna-isaac commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

A bug that needs fixing is that sidebar navigation is broken on http://localhost:8004/test/attempt/biology_summer_challenge_1. On the sidebar, clicking Sections -> Test questions takes to a non-existent page.

@barna-isaac

Copy link
Copy Markdown
Contributor

With the changes, the /practice_tests page looks very misaligned on Ada.

Screenshot 2026-06-24 at 11 25 35

@barna-isaac

barna-isaac commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

I'm not sure whether this change is intentional, but when viewing feedback for a test, Ada used to always hide the "
Show instructions" button. This button is now shown on Ada, even when showing feedback. I don't think this is a very important change, but maybe it was hidden on Ada because they've specifically asked that we hide it? At any rate, unless you made the change by mistake, I think it's fine to show the button.

const deviceSize = useDeviceSize();
const sections = attempt.quiz?.children;
const section = sections && sections[page - 1];
const section = !!(page && sections) && sections[page - 1];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be page && sections && sections[page - 1]. The inferred type for section becomes messier, but the check on line 206 only leaves ContenBaseDto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could even be page && sections?.[page - 1]; if we want extra shortening!

I most likely did this as you say to tidy up the inferred type, but idrm.

export const teacherQuizzesCrumbs = [{title: siteSpecific("Set / manage tests", "Tests"), to: `/set_tests`}];
export const rubricCrumbs = [{title: "Practice tests", to: "/practice_tests"}];
const getCrumbs = (preview: boolean | undefined, view: boolean | undefined, user: RegisteredUserDTO) => {
export const viewQuizzesCrumbs = [{title: "View tests", to: "/view_tests"}];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd comment out this line too, as this is just an unused variable until we implement the TODO. A pity that eslint doesn't detect unused exports!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we comment it out, we'll need to uncomment it when we do the merge which uses this (#2228). I'm not intending one to exist in main without the other, so yes while I shouldn't have added this on this branch, now it's here let's just leave it.


const questions = attempt ? props.questions : [];
const sections = attempt ? props.sections : {};
const questions = isFullQuizProps(props) ? (props as QuizProps & FullQuizInfo).quizContents.questions : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need these casts, that's the whole point of isFullQuizProps!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot. When I first wrote isFullQuizProps it was a function over the quiz, so this looked like

isFullQuiz(quiz) ? (props as QuizProps & FullQuizInfo).quizContents.questions : [];

which does need the cast.

const navigate = useNavigate();
const location = useLocation();

const isFullQuiz = (quiz: QuizSidebarProps['quiz']): quiz is IsaacQuizDTO => isDefined((quiz as IsaacQuizDTO).canonicalSourceFile);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be defined outside of the component. the performance penalty of defining it within the component is minimal (I certainly wouldn't want to optimise this, eg. usign useMemo), but I wonder if you agree, in general, that defining this outside the component would improve readability? for me, instantly knowing the function doesn't have any component state in its closure would be help by itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's a function definition, I don't think redeclaring it every render is a bad thing; after all, everything gets redeclared, but hooks etc are made to be idempotent under multiple runs with the same params such that they point to the same object. rubricPath, hasSections, etc. are all also variables that would be recalculated each render.

It's more of a code style thing – but yes, I do agree that declaring helper functions outside is a useful thing so happy to make the change.

@jacbn

jacbn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

I'm not sure whether this change is intentional, but when viewing feedback for a test, Ada used to always hide the "
Show instructions" button.

It was hidden on Ada if you had completed the test:

const renderRubric = (rubric?.children || []).length > 0 && (isPhy || !isDefined(attempt.completedDate));

Since Ada haven't been using tests until very recently, there is no way anyone on their side requested this behaviour specifically for Ada. I've made the decision to simplify things by aligning the two sites, preferring the Science defaults. Do you think this is a bad idea?

@barna-isaac

barna-isaac commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

I'm not sure whether this change is intentional, but when viewing feedback for a test, Ada used to always hide the "
Show instructions" button.

It was hidden on Ada if you had completed the test:

const renderRubric = (rubric?.children || []).length > 0 && (isPhy || !isDefined(attempt.completedDate));

Since Ada haven't been using tests until very recently, there is no way anyone on their side requested this behaviour specifically for Ada. I've made the decision to simplify things by aligning the two sites, preferring the Science defaults. Do you think this is a bad idea?

No I think it's fine to show the button and I'm glad we're simplifying stuff, I just wasn't sure why it was hidden from Ada in the first place (and whether bringing it back was intentional). Happy with the change, as long as it was made knowingly!

@jacbn

jacbn commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

I've made a couple of changes. To address your first point about it being more difficult for teachers to attempt a test themselves – I agree, but I don't want it to be as visible (i.e. on the ALVIs) – so you can now try the quiz yourself from the /preview/ link. I've also fixed the bug you mentioned surrounding the sidebar links – thanks for spotting. And lastly, many of the styling issues I had already fixed in the "future" branch set-manage-all-work-types; I've copied these (not cherry-picked, as they weren't identical) in for now, hopefully a merge in that one won't break anything.

I wonder if this might misleadingly suggest to students that an attempt on the test has already started

I don't think this distinction matters to them; as long as it isn't filling up their My Tests list, which was the reason we wanted the /view - /attempt split in the first place, I think it's okay.

Thanks for the detailed review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants