Skip to content

Commit cc0319d

Browse files
authored
fix: clean up plugin setup script tags (#7750)
* fix: clean up plugin setup script tags Remove temporary plugin setup script elements from document.head after load or load failure so repeated plugin loads do not leave stale DOM nodes behind. Related to #7388 * test: verify plugin setup script execution
1 parent 38abf23 commit cc0319d

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright (c) 2026 Music Blocks contributors
2+
//
3+
// This program is free software; you can redistribute it and/or
4+
// modify it under the terms of the The GNU Affero General Public
5+
// License as published by the Free Software Foundation; either
6+
// version 3 of the License, or (at your option) any later version.
7+
//
8+
// You should have received a copy of the GNU Affero General Public
9+
// License along with this library; if not, write to the Free Software
10+
// Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
11+
12+
const createActivity = () => ({
13+
blocks: {
14+
protoBlockDict: {}
15+
},
16+
logo: {
17+
evalFlowDict: {},
18+
evalArgDict: {},
19+
evalSetterDict: {},
20+
evalParameterDict: {},
21+
evalOnStartList: {},
22+
evalOnStopList: {}
23+
},
24+
palettes: {
25+
buttons: {},
26+
add: jest.fn(),
27+
makePalettes: jest.fn(),
28+
updatePalettes: jest.fn(),
29+
show: jest.fn(),
30+
pluginMacros: {}
31+
},
32+
pluginsImages: {}
33+
});
34+
35+
describe("processPluginData script cleanup", () => {
36+
let processPluginData;
37+
let originalCreateObjectURL;
38+
let originalRevokeObjectURL;
39+
let originalBlob;
40+
let appendChildSpy;
41+
let blobUrls;
42+
let blobUrlIndex;
43+
44+
beforeEach(() => {
45+
jest.useFakeTimers();
46+
jest.resetModules();
47+
document.head.innerHTML = "";
48+
49+
global._ = msg => msg;
50+
global.PALETTEICONS = {};
51+
global.PALETTEFILLCOLORS = {};
52+
global.PALETTESTROKECOLORS = {};
53+
global.PALETTEHIGHLIGHTCOLORS = {};
54+
global.HIGHLIGHTSTROKECOLORS = {};
55+
global.MULTIPALETTES = [[], [], []];
56+
global.platformColor = { paletteColors: {} };
57+
window.__mb_plugin_registry = {};
58+
59+
originalCreateObjectURL = URL.createObjectURL;
60+
originalRevokeObjectURL = URL.revokeObjectURL;
61+
originalBlob = global.Blob;
62+
global.Blob = class {
63+
constructor(parts) {
64+
this.parts = parts;
65+
}
66+
67+
text() {
68+
return Promise.resolve(this.parts.join(""));
69+
}
70+
};
71+
blobUrls = new Map();
72+
blobUrlIndex = 0;
73+
URL.createObjectURL = jest.fn(blob => {
74+
const url = `blob:plugin-setup-${blobUrlIndex}`;
75+
blobUrlIndex += 1;
76+
blobUrls.set(url, blob);
77+
return url;
78+
});
79+
URL.revokeObjectURL = jest.fn();
80+
81+
({ processPluginData } = require("../utils.js"));
82+
});
83+
84+
afterEach(() => {
85+
if (appendChildSpy) {
86+
appendChildSpy.mockRestore();
87+
appendChildSpy = undefined;
88+
}
89+
URL.createObjectURL = originalCreateObjectURL;
90+
URL.revokeObjectURL = originalRevokeObjectURL;
91+
global.Blob = originalBlob;
92+
document.head.innerHTML = "";
93+
delete window.__mb_plugin_registry;
94+
delete globalThis.pluginSetupLoaded;
95+
jest.useRealTimers();
96+
});
97+
98+
it("removes setup script elements after they load", async () => {
99+
const originalAppendChild = document.head.appendChild.bind(document.head);
100+
appendChildSpy = jest.spyOn(document.head, "appendChild").mockImplementation(script => {
101+
originalAppendChild(script);
102+
blobUrls
103+
.get(script.src)
104+
.text()
105+
.then(code => {
106+
Function(code)();
107+
script.onload();
108+
});
109+
return script;
110+
});
111+
112+
await processPluginData(
113+
createActivity(),
114+
JSON.stringify({
115+
BLOCKPLUGINS: {
116+
testBlock: "globalThis.pluginSetupLoaded = true;"
117+
}
118+
}),
119+
"plugins/test.json"
120+
);
121+
122+
expect(globalThis.pluginSetupLoaded).toBe(true);
123+
expect(document.head.querySelectorAll("script[src^='blob:plugin-setup']")).toHaveLength(0);
124+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:plugin-setup-0");
125+
});
126+
127+
it("removes setup script elements after load errors", async () => {
128+
const originalAppendChild = document.head.appendChild.bind(document.head);
129+
appendChildSpy = jest.spyOn(document.head, "appendChild").mockImplementation(script => {
130+
originalAppendChild(script);
131+
script.onerror(new Error("load failed"));
132+
return script;
133+
});
134+
135+
await processPluginData(
136+
createActivity(),
137+
JSON.stringify({
138+
BLOCKPLUGINS: {
139+
testBlock: "globalThis.pluginSetupLoaded = true;"
140+
}
141+
}),
142+
"plugins/test.json"
143+
);
144+
145+
expect(document.head.querySelectorAll("script[src^='blob:plugin-setup']")).toHaveLength(0);
146+
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:plugin-setup-0");
147+
});
148+
});

js/utils/utils.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ if (typeof module !== "undefined" && module.exports) {
4343
docBySelector, docByTagName, doPublish, doStopVideoCam, doSVG,
4444
doUseCamera, format, getTextWidth, hideDOMLabel, httpGet, httpPost, HttpRequest,
4545
importMembers, isSVGEmpty, prepareMacroExports, preparePluginExports,
46-
processMacroData, processRawPluginData, windowHeight, windowWidth,
46+
processMacroData, processPluginData, processRawPluginData, windowHeight, windowWidth,
4747
fnBrowserDetect, waitForReadiness
4848
*/
4949

@@ -966,10 +966,16 @@ window.__mb_plugin_registry["${registryName}"] = function(activity, globalActivi
966966
delete window.__mb_plugin_registry[registryName];
967967
}
968968
URL.revokeObjectURL(sUrl);
969+
if (sScript.parentNode) {
970+
sScript.parentNode.removeChild(sScript);
971+
}
969972
resolve();
970973
};
971974
sScript.onerror = () => {
972975
URL.revokeObjectURL(sUrl);
976+
if (sScript.parentNode) {
977+
sScript.parentNode.removeChild(sScript);
978+
}
973979
resolve(); // Still resolve to let others run
974980
};
975981
document.head.appendChild(sScript);
@@ -1535,6 +1541,7 @@ if (typeof module !== "undefined" && module.exports) {
15351541
doSVG,
15361542
isSVGEmpty,
15371543
prepareMacroExports,
1544+
processPluginData,
15381545
processMacroData,
15391546
hideDOMLabel,
15401547
displayMsg

0 commit comments

Comments
 (0)