Skip to content

Show QTI items whose body the editor can't rebuild as unsupported - #6288

Merged
AlexVelezLl merged 2 commits into
learningequality:unstablefrom
rtibblesbot:issue-6225-0e0679
Oct 7, 2026
Merged

AlexVelezLl merged 2 commits into
learningequality:unstablefrom
rtibblesbot:issue-6225-0e0679

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Items whose body the editor can't rebuild are read-only with unsupportedItemMessage$(); raw_data untouched.

References

Closes #6225. Extends #6105.

Reviewer guidance

  • Check textEntry/Descriptor.js isSupportedBody against ricecooker bodies.
  • Developer check: on a channel's #/qti-demo page, load a choice item with a paragraph before the interaction and no correct answer. It reads "This question cannot be edited here" with no Incomplete.

QA team: questions that Studio cannot rebuild now show read-only and no longer count as incomplete. A regression is most probable in the Incomplete flags of questions that Studio can edit.

All items below use one new exercise in a channel that you can edit. Sign in as a channel editor.

  1. Incomplete flags on editable questions
    • Add a "Single Choice" question with answer options and no correct answer.
    • Open the Questions tab. The banner reads "1 incomplete question". The question shows Incomplete.
    • Go back to the channel. The exercise card shows the error icon.
    • Click Edit on the question and select a correct answer. Reload the channel. The card shows no error icon.
  2. Text entry questions
    • Add a "Text entry" question with a prompt.
    • Click Add acceptable answer and type an answer.
    • Close the exercise and open it again. The answer shows, and Edit opens the answer controls.

No content that QA can open holds a question that Studio cannot rebuild. Look at the first screenshot under "Evidence". Is the read-only message "This question cannot be edited here" clear without an Incomplete flag?

Evidence

Unsupported items that break the rules, beside an incomplete editable item

Step Screenshot
Unsupported items read-only, no Incomplete flag Unsupported items read-only, no Incomplete flag
Before fixing the editable item: banner counts only it Before fixing the editable item: banner counts only it
Before fixing the editable item: card shows the error icon Before fixing the editable item: card shows the error icon
After fixing it: card shows no error icon After fixing it: card shows no error icon
fix-supported-item-clears-incomplete.webm

Kept editable: converted text entry

Step Screenshot
Converted text entry, answer added Converted text entry, answer added

Unsupported body shapes

Step Screenshot
Paragraph beside a choice interaction: unsupported Paragraph beside a choice interaction: unsupported
Unsupported text entry shapes Unsupported text entry shapes
More captures (7)
Step Screenshot
After fixing the editable item: no incomplete banner After fixing the editable item: no incomplete banner
Studio-authored single choice opens editable Studio-authored single choice opens editable
Converted choice opens editable Converted choice opens editable
Whitespace-padded choice opens editable Whitespace-padded choice opens editable
Text entry open in editor Text entry open in editor
Choice still unsupported after reopening Choice still unsupported after reopening
Text entry unsupported after reopening Text entry unsupported after reopening

AI usage

Claude Code wrote the checks and tests with TDD and reviewed the diff.

🤖 Generated with Claude Code


Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-06 23:54 UTC

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Outside this PR's scope — tracked in:


Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Outside this PR's scope — tracked in:


Written by rtibblesbot, an LLM-based coding agent.

- Block interactions: the body must hold only the interaction
- Text entry: the interaction alone in a <p>, nothing after it
- Attributes on the body or the text entry's <p> make the item unsupported

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rtibblesbot
rtibblesbot marked this pull request as ready for review October 5, 2026 17:50
@rtibblesbot
rtibblesbot requested review from a team and leq-bot October 5, 2026 17:50
// Shown read-only: the editor's rules don't apply, only unreadable interactions count.
if (item.interactions.length && !isSupportedInteraction(item.interactions)) {
// The editor's rules don't apply, only unreadable interactions count. Body shape alone
// doesn't exempt an item: its interaction is still one those rules cover.

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: Applying the editor's rules to items the editor won't open creates questions that are Incomplete and can't be fixed. Repro, the issue's own fill-in-the-blank shape with no separate prompt:

<qti-item-body><p>Distance: <qti-text-entry-interaction response-identifier="RESPONSE"/> km</p></qti-item-body>

isSupportedItem returns false, so the card is read-only. validateQtiItem still returns [{ code: 'PROMPT_REQUIRED' }], because the text entry parser finds no prompt outside the interaction's <p>. The card shows Incomplete, Edit is disabled, and the exercise can't be published. The only fix is re-uploading through ricecooker. A choice item with a stimulus and no correct answer ends up in the same state (Question 2 in the "Incomplete only on missing correct answer" screenshot).

The issue says these items "are kept intact and stay publishable", and AC4 says they're treated as they are for the other unsupported items from #6105, where only unreadable XML counts. Here that means validateQtiItem should use the full check:

if (item.interactions.length && !isSupportedItem(item.interactions, item.itemBodyXml)) {

Then isSupportedInteraction has no callers and editableDescriptor can stay private. The isIncomplete comment in QTIItemEditor can go back to its original wording, and the "applies editor rules" test and the Incomplete-indicator test would expect no errors. If you think a stimulus item missing its correct answer should still block publishing, please raise that on #6225 first, since it means an item nobody can fix in Studio.

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.

Fixed in 3895ab8: validateQtiItem uses isSupportedItem(item.interactions, item.itemBodyXml); isSupportedInteraction removed, editableDescriptor private, isIncomplete comment restored.

  • Searched every caller of validateQtiItem/isSupportedItem/validateItemShape: 3 non-test sites. shared/utils/validation.js goes through validateQtiItem, so it is fixed by the same change; QTIItemEditor only calls validateItemShape for supported items.
  • Your no-prompt fill-in-the-blank repro and the stimulus-without-correct-answer item now return no errors; the Incomplete-indicator test expects no indicator.

Comment on lines +46 to +54
it('applies editor rules to an item whose body the editor cannot reproduce', () => {
expect(validateQtiItem(CHOICE_ITEM_DOCUMENT_NO_CORRECT_ANSWER_WITH_STIMULUS)).toEqual([
{ code: ValidationError.NO_CORRECT_ANSWER },
]);
expect(
validateQtiItem(TEXT_ENTRY_ITEM_DOCUMENT_SHARED_PARAGRAPH, { allowFreeResponse: false }),
).toEqual([{ code: ValidationError.FREE_RESPONSE_NOT_ALLOWED }]);
});

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 incorrect. If the item cannot be edited, no errors should be raised, because these errors block it from being published, and given that the Editor cannot edit it, users have no way to fix it or see what is wrong with 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.

Fixed in 3895ab8: the test now expects [] for both, and covers your no-prompt fill-in-the-blank shape with allowFreeResponse: false.

Comment on lines +62 to +88
/**
* The builder writes the prompt, then a `<p>` holding only the interaction. Anything else
* would be dropped from the interaction's `<p>`, or moved ahead of it if it follows.
* Legacy conversion wraps the same shape in a bare `<div>`, the body's only content.
*
* @param {Element} bodyEl
* @returns {boolean}
*/
isSupportedBody(bodyEl) {
const paragraph = bodyEl.querySelector(this.type).parentElement;
if (
paragraph.localName !== 'p' ||
hasNonNamespaceAttributes(paragraph) ||
contentOf(paragraph).length > 1 ||
hasContentAfter(paragraph)
) {
return false;
}
const container = paragraph.parentElement;
return (
container === bodyEl ||
(container.localName === 'div' &&
!hasNonNamespaceAttributes(container) &&
container.parentElement === bodyEl &&
contentOf(bodyEl).length === 1)
);
}

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 also have the implementation of this in the parse.js module instead? Just like parse and build, so that all XML parsing logic (and helpers) is there.

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.

Moved in 3895ab8 to isSupportedTextEntryBody in textEntry/parse.js, with its helpers; the descriptor delegates.

  • Searched every Descriptor.js for DOM work this PR added: only this one. The block-shape default in InteractionDescriptor.isSupportedBody stays, as it is shared by all block descriptors and has no parse.js.

- validateQtiItem uses isSupportedItem, so read-only items aren't left Incomplete
- Move the text entry body-shape check into parse.js

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

Copy link
Copy Markdown
Contributor Author

Your two repros after 3895ab8: no-prompt fill-in-the-blank (Q1) and stimulus + choice with no correct answer (Q2). Both read-only, neither Incomplete, no error on the Questions tab.

Both unsupported questions without the Incomplete label

@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.

Code changes look good, manual QA checks out! LGTM!

@AlexVelezLl
AlexVelezLl merged commit 90f7729 into learningequality:unstable Oct 7, 2026
28 checks passed
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Quick check on rtibblesbot's work on this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific.

@rtibblesbot
rtibblesbot deleted the issue-6225-0e0679 branch October 7, 2026 14:55
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 items whose body the editor can't reproduce as unsupported

2 participants