Skip to content

Commit 960aeaf

Browse files
authored
Merge pull request #3545 from scratchfoundation/fix/xml-load-diagnostics
fix: improve error response when loading XML
2 parents d8a9f91 + 904cd6b commit 960aeaf

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

src/xml.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,29 @@ export function clearWorkspaceAndLoadFromXml(xml: Element, workspace: Blockly.Wo
3636
xml.querySelector('variables')?.remove()
3737

3838
// Defer to core for the rest of the deserialization.
39-
const blockIds = Blockly.Xml.domToWorkspace(xml, workspace)
39+
let blockIds: string[]
40+
try {
41+
blockIds = Blockly.Xml.domToWorkspace(xml, workspace)
42+
} catch (error) {
43+
const context =
44+
Array.from(xml.children)
45+
.map((el) => {
46+
const type = el.getAttribute('type')
47+
const id = el.getAttribute('id')
48+
const attrs = [...(type ? [`type=${type}`] : []), ...(id ? [`id=${id}`] : [])]
49+
return attrs.length ? `${el.tagName}[${attrs.join(', ')}]` : el.tagName
50+
})
51+
.join(', ') || '(none)'
52+
const message = error instanceof Error ? error.message : String(error)
53+
const wrapped = new Error(
54+
`Failed to load workspace XML (${message}). Top-level elements: ${context}`,
55+
) as Error & { cause?: unknown }
56+
wrapped.cause = error
57+
throw wrapped
58+
} finally {
59+
Blockly.Events.setGroup(false)
60+
workspace.setResizesEnabled(true)
61+
}
4062

41-
workspace.setResizesEnabled(true)
4263
return blockIds
4364
}

tests/browser/xml.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,50 @@ describe('clearWorkspaceAndLoadFromXml — isLocal and isCloud preservation', ()
117117
expect(global_.isCloud).toBe(false)
118118
})
119119

120+
it('wraps domToWorkspace errors with top-level element context', () => {
121+
// A top-level <shadow> will cause Blockly to throw.
122+
const xml = parseXml(`
123+
<xml>
124+
<shadow type="looks_costume" id="badShadow" x="0" y="0"></shadow>
125+
</xml>
126+
`)
127+
128+
let caught: unknown
129+
try {
130+
clearWorkspaceAndLoadFromXml(xml, workspace)
131+
} catch (e) {
132+
caught = e
133+
}
134+
expect(caught).toBeDefined()
135+
expect((caught as Error).message).toMatch(/Failed to load workspace XML/)
136+
expect((caught as Error).message).toMatch(/badShadow/)
137+
})
138+
139+
it('closes the event group and re-enables resizes after a load failure', () => {
140+
const xml = parseXml(`
141+
<xml>
142+
<shadow type="looks_costume" id="badShadow" x="0" y="0"></shadow>
143+
</xml>
144+
`)
145+
146+
try {
147+
clearWorkspaceAndLoadFromXml(xml, workspace)
148+
} catch (_e) {
149+
// expected
150+
}
151+
152+
// The event group opened by clearWorkspaceAndLoadFromXml should be closed.
153+
// Firing an event outside of a group verifies that setGroup(false) was called.
154+
// If the group were still open, this event would be part of it.
155+
const event = new (Blockly.Events.get(Blockly.Events.VAR_CREATE))(
156+
workspace.getVariableMap().createVariable('testVar', '', 'testId'),
157+
)
158+
expect(event.group).toBeFalsy()
159+
160+
// Resizes should be re-enabled after a load failure.
161+
expect((workspace as unknown as { resizesEnabled: boolean }).resizesEnabled).toBe(true)
162+
})
163+
120164
it('clears the existing workspace before loading', () => {
121165
// Pre-load a variable.
122166
workspace.getVariableMap().createVariable('old', '', 'oldId')

0 commit comments

Comments
 (0)