Repository navigation
Show QTI items whose body the editor can't rebuild as unsupported - #6288
Conversation
|
Outside this PR's scope — tracked in:
Written by rtibblesbot, an LLM-based coding agent. |
|
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>
77650ea to
636c271
Compare
| // 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.jsgoes throughvalidateQtiItem, so it is fixed by the same change;QTIItemEditoronly callsvalidateItemShapefor 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.
| 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 }]); | ||
| }); | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 3895ab8: the test now expects [] for both, and covers your no-prompt fill-in-the-blank shape with allowFreeResponse: false.
| /** | ||
| * 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) | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Moved in 3895ab8 to isSupportedTextEntryBody in textEntry/parse.js, with its helpers; the descriptor delegates.
- Searched every
Descriptor.jsfor DOM work this PR added: only this one. The block-shape default inInteractionDescriptor.isSupportedBodystays, as it is shared by all block descriptors and has noparse.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>
|
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. |
AlexVelezLl
left a comment
There was a problem hiding this comment.
Code changes look good, manual QA checks out! LGTM!
|
Quick check on rtibblesbot's work on this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific. |

Summary
Items whose body the editor can't rebuild are read-only with
unsupportedItemMessage$();raw_datauntouched.<p>, nothing after it.<qti-item-body>(e.g.dir="rtl") make the item unsupported.<div>(legacyconvert.pyshape) stays editable.References
Closes #6225. Extends #6105.
Reviewer guidance
textEntry/Descriptor.jsisSupportedBodyagainst ricecooker bodies.#/qti-demopage, 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.
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
fix-supported-item-clears-incomplete.webm
Kept editable: converted text entry
Unsupported body shapes
More captures (7)
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.
🟡 Waiting for feedback
Last updated: 2026-10-06 23:54 UTC