Skip to content

Show the unsupported message for QTI items with multiple or unrecognized interactions - #6218

Merged
AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6105-041d67
Oct 2, 2026
Merged

AlexVelezLl merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6105-041d67

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #6105

Summary

QTI items without exactly one editable interaction show unsupportedItemMessage$() in a read-only card; raw_data is never rewritten.

  • QtiInteraction lists every QTI 3.0 interaction.
  • resolveDescriptor returns descriptor: null on no match; DEFAULT_INTERACTION is removed.
  • Unreadable or empty QTI still blocks publishing; valid but uneditable QTI is publishable (maintainer decision).

Reviewer guidance

  • Multiple text-entry blanks: supported.
  • Three-set match: "could not be loaded".
  • No raw_data: content placeholder.
  • Unsupported cards keep Close in edit mode.
  • Setup: pnpm devsetup, pnpm devserver; seed utils/testingFixtures.js via the browser data layer.
  • Forced mode="edit" in QTIDemoPage.vue: unsupported items show only CLOSE.
  • Steps 7–8 reload unverified this round: dev server 500 (Redis).

QA steps

  1. QTI demo page (/en/channels/<channel id>/#/qti-demo), two-interaction and extended-text fixtures, Show answers ticked: both read "This question cannot be edited here"; Edit disabled.
  2. Exercise QUESTIONS tab, blank, no-interaction, malformed QTI and Perseus fixtures: all but Perseus show "Incomplete"; FINISH warns "1 incomplete resource found".
  3. Exercise QUESTIONS tab, only a Perseus fixture: no "Incomplete"; FINISH closes the editor.
  4. Exercise QUESTIONS tab, single-choice fixture with hints: Show answers adds Answer options and Hints.
  5. Exercise QUESTIONS tab, single-choice fixture: Edit, change prompt and answer, CLOSE, reload: card shows new text, not fixture text.
  6. Exercise QUESTIONS tab, two-blank text-entry fixture: Edit opens the full editor.
  7. Exercise Edit details → QUESTIONS, one fixture per editable type: click a prompt, cycle Response type back to the original: each type's full editor, prompt kept; free response shows "only available on surveys". Edit prompt, Add choice / Add acceptable answer, CLOSE, reload: header "Question N of 9 — ", edited text, not fixture text.
  8. Exercise Edit details → QUESTIONS, single-choice fixture: New question, pick a Response type, author it, CLOSE, reload: new prompt starts empty, full editor; fixture keeps its text.

Evidence

Switching and editing each editable type

Step Screenshot
Single choice → Associate, prompt kept Single choice → Associate, prompt kept
Ordering: full editor Ordering: full editor
Two-set match: full editor Two-set match: full editor
Free response → Text entry Free response → Text entry
After CLOSE: type in header After CLOSE: type in header
More captures (26)
Step Screenshot
Two-interaction: unsupported Two-interaction: unsupported
Forced edit: only CLOSE Forced edit: only CLOSE
"Incomplete" badges Incomplete badges
FINISH dialog FINISH dialog
Perseus alone Perseus alone
Answers and hints Answers and hints
Choice editor Choice editor
Two-blank text entry Two-blank text entry
Answers off Answers off
Hints collapsed Hints collapsed
Choice edited Choice edited
Edits after reload Edits after reload
Text entry hints Text entry hints
Nine questions Nine questions
Response type help Response type help
Multiple Choice Multiple Choice
Numeric Numeric
Text entry Text entry
Ordering Ordering
Match Match
Question 1 edited Question 1 edited
Multiple choice opened Multiple choice opened
Numeric opened Numeric opened
Numeric answer Numeric answer
Text entry opened Text entry opened
Associate opened Associate opened

🤖 Generated with Claude Code


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-02 06:49 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review September 30, 2026 15:05
@rtibblesbot
rtibblesbot requested review from a team and marcellamaki September 30, 2026 15:05
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Outside this PR's scope — tracked under #6103:


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks

@rtibblesbot
rtibblesbot force-pushed the issue-6105-041d67 branch 6 times, most recently from 15fecdc to dfe73bf Compare September 30, 2026 18:36
@rtibblesbot
rtibblesbot force-pushed the issue-6105-041d67 branch 2 times, most recently from b7b05db to e655cc1 Compare September 30, 2026 23:26
Comment on lines +57 to +59
it('does not block an extended-text item on a consumer that scores its questions', () => {
expect(validateQtiItem(EXTENDED_TEXT_ITEM_DOCUMENT, { allowFreeResponse: false })).toEqual([]);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"extended-text item on a consumer that scores its questions" is not clear, and it's not clear why the allowFreeResponse is relevant for this test.

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.

Removed that test; its allowFreeResponse case now runs inside the read-only-item table (allowFreeResponse true and false), so no extended-text fixture is needed. Searched the QTIEditor specs for other tests tied to a specific interaction: resolveDescriptor.spec, parseItem.spec and QTIItemEditor.spec had the same assumption (3 places), all changed.


it.each([
['several interactions', MULTI_INTERACTION_ITEM_DOCUMENT],
['an interaction with no descriptor', UNRECOGNIZED_INTERACTION_ITEM_DOCUMENT],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we remove the assumption that the hotspot is an interaction with no descriptor? If, in the future, we implement it and have a descriptor, and the fixture is valid, we will also get an empty array, so this is not correct. We can iterate through the registry and pick an interaction not described by any descriptor's type field, and that's it.

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.

Added UNDESCRIBED_INTERACTION to the fixtures: the first QtiInteraction no registered descriptor's type matches. The unrecognized-item fixture is built from it, and the hotspot/extended-text literals are gone from validateItem.spec, resolveDescriptor.spec, parseItem.spec and QTIItemEditor.spec (the extended-text fixture is deleted).

import { isSupportedItem } from '../../interactions/resolveDescriptor';
import InteractionSection from '../InteractionSection/index.vue';
import HintsSection from '../HintsSection/index.vue';
import { getAssessmentItemErrors } from 'shared/utils/validation';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, the QTIEditor component should be extractable and must not import anything from Studio, find a better way.

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.

Dropped the shared/utils/validation import. The card now calls the QTIEditor's own validateQtiItem for unsupported QTI. Grepped the non-test QTIEditor sources for other shared/ imports: none other than the existing shared/views/TipTapEditor/KDS ones, which I left alone.

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.

Correction: the grep found other shared/ imports (i18n, strings, TipTapEditor, dragSort) in files this PR doesn't touch. I left those as is; only the import this PR added is removed.

responseDeclarations: currentResponseDeclarations,
},
);
const { interactions, hints, parseError, rawData } = useQtiItem(props.item.raw_data, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we don't use the itemBodyXml anymore, let's also drop it from the useQtiItem composable.

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.

Dropped itemBodyXml from useQtiItem (state, assignment, JSDoc). parseItem still returns it; convertedItem.spec reads it from there. No other consumers.

<!-- .stop: expanding or collapsing the hints must not open the card -->
<HintsSection
v-if="hasHints && (mode === 'edit' || showAnswers)"
v-if="hasHints && !isUnsupported && (mode === 'edit' || showAnswers)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is becoming a bit complex; could we have a computed property instead and tweak the conditions so that they are more readable?

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.

Split into computeds: isQti, isEditableQti (readable and blank-or-single-known-interaction), and isUnsupported = !isQti || !isEditableQti. isIncomplete reuses isQti.

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.

The hints v-if is now a showHints computed (hasHints && !isUnsupported && (mode === 'edit' || showAnswers)). Searched the template conditions this PR adds or changes for other compound expressions: no other match.

@rtibblesbot
rtibblesbot force-pushed the issue-6105-041d67 branch 3 times, most recently from 2cba6aa to 12d0bb5 Compare October 1, 2026 15:14
return false;
}
const [{ bodyXml, responseDeclarations }] = interactions;
return resolveDescriptor(bodyXml, responseDeclarations).descriptor !== null;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

blocking: This counts an interaction as supported when its descriptor resolved but resolveDescriptor also returned an error, which is what happens for a match with three sets (getQuestionType throws, and the catch keeps the descriptor). The card and node-level validation then disagree on that item:

  • Card: isEditableQti is true, so it doesn't take the unsupported branch. InteractionSection shows "could not be loaded", mounts no editor and emits no errors, and currentQuestionType stays null. isIncomplete is therefore false.
  • Node: getAssessmentItemErrors → validateQtiItem → isSupportedItem true → the loop pushes PARSE_ERROR, so the item counts as invalid.

I confirmed this by rendering the card for every fixture case, with allowFreeResponse true and false, and comparing the badge with getAssessmentItemErrors: 22/24 cases match, and the two mismatches are the three-set match. Two more consequences: the incomplete-questions count includes an item whose card shows no badge, and canOpen is true, so Edit opens a card that only shows a parse error.

This predates the PR (on unstable, the unparseable-XML case also disagreed, and this PR fixes that one), but the new rule says unreadable QTI blocks publishing, and this item doesn't block it on the card.

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.

isSupportedItem now also requires resolveDescriptor to return no error, so a three-set match is unsupported on the card (read-only, incomplete badge via validateQtiItem) and on the node, which agree. canOpen is false for it too. The resolveDescriptor.spec case that accepted it now rejects it, and QTIItemEditor.spec has a card test for the badge. Searched the callers of isSupportedItem and resolveDescriptor: 2 callers of isSupportedItem (card, validateQtiItem), both go through the changed function; useInteractionDescriptor only runs for supported items. Nothing else needed changing.

@rtibblesbot
rtibblesbot force-pushed the issue-6105-041d67 branch 2 times, most recently from f91fd1e to 544f873 Compare October 1, 2026 17:08
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Pushed two behaviour changes from self-review:

  • An item with several qti-text-entry-interactions in one body is now unsupported. Before this, editing it saved only one blank and dropped the others. SINGLE_INSTANCE_INTERACTIONS in descriptors.js is what drives this; the card and validateQtiItem both reach it through isSupportedItem.
  • In view mode with answers shown, an unsupported item's hints now show read-only. Edit mode still hides them.

No screenshot: the browser driver (drive-browser.py) is not installed in the agent's sandbox.

rtibblesbot and others added 3 commits October 2, 2026 05:44
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add isSupportedItem: exactly one interaction block whose descriptor has
an editor and reads its question type without error. Text entry counts
per interaction, since the editor holds one blank per item.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…items

The card renders no editable controls, including hints, and never
rewrites the item's XML. Unreadable or empty QTI still blocks publishing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm!

@AlexVelezLl
AlexVelezLl merged commit 72e4673 into learningequality:unstable Oct 2, 2026
13 checks passed
@rtibblesbot
rtibblesbot deleted the issue-6105-041d67 branch October 2, 2026 16:33
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.

[QTI] Show the unsupported message for QTI items with multiple or unsupported interactions

2 participants