Skip to content

Commit ab08a77

Browse files
committed
fix(userscript): keep download buttons working after result re-renders
Site handlers observed a replaceable container node (e.g. the pack table's parent on xdcc.info). When a filter such as "language" re-rendered the results, the observer kept watching a detached node and injected buttons never came back until a full reload. Observe document.body in all dynamic-site handlers so the observer survives container replacement, re-query .results inside processRocksTable for the same reason, and guard observer callbacks against firing during page teardown. Bump userscript version to 2026-09-20-1.
1 parent 18f1c04 commit ab08a77

2 files changed

Lines changed: 52 additions & 15 deletions

File tree

‎tests/js/userscript/xdcc-info.test.js‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,32 @@ describe("userscript on www.xdcc.info", () => {
7979
});
8080
});
8181

82+
test("re-injects buttons when the results container is replaced", async () => {
83+
// The language filter re-renders the whole results container, detaching
84+
// the node the observer was watching.
85+
document.body.innerHTML = `
86+
<div class="results"><table class="pack-table"><tbody>
87+
${ROW("BotA", "1", "irc.rizon.net", "#chan")}
88+
</tbody></table></div>`;
89+
loadScript();
90+
expect(dccbotButtons()).toHaveLength(1);
91+
92+
document.querySelector(".results").outerHTML = `
93+
<div class="results"><table class="pack-table"><tbody>
94+
${ROW("NewBot", "42", "irc.abjects.net", "#beast-xdcc")}
95+
</tbody></table></div>`;
96+
await new Promise((r) => setTimeout(r, 0));
97+
98+
const buttons = dccbotButtons();
99+
expect(buttons).toHaveLength(1);
100+
expect(buttons[0].dataset).toMatchObject({
101+
server: "irc.abjects.net",
102+
channel: "#beast-xdcc",
103+
bot: "NewBot",
104+
pack: "42",
105+
});
106+
});
107+
82108
test("runs on the bare xdcc.info domain too", () => {
83109
// hostname is fixed by the environment URL; covered by the dispatch table
84110
// indirectly — see hostHandlers for 'xdcc.info'.

‎userscript/add-dccbot-btn.js‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// @name add-dccbot-btn
33
// @namespace https://github.com/luni/dccbot/
44
// @website https://github.com/luni/dccbot/
5-
// @version 2026-09-20
5+
// @version 2026-09-20-1
66
// @description Add button for DCCbot to automate downloads.
77
// @author luni
88
// @match https://www.xdcc.eu/search.php*
@@ -139,7 +139,11 @@
139139

140140
function observeMutations(target, onMutation, options) {
141141
if (!target) return null;
142-
const observer = new MutationObserver(onMutation);
142+
const observer = new MutationObserver(function () {
143+
// Queued callbacks can fire during page unload/teardown.
144+
if (typeof document === "undefined" || !document.body) return;
145+
onMutation();
146+
});
143147
observer.observe(target, options || { childList: true, subtree: true });
144148
return observer;
145149
}
@@ -891,7 +895,9 @@
891895
return;
892896
}
893897

894-
observeMutations(tableBody, scheduleProcess);
898+
// Observe <body>: the results table may be replaced on re-render,
899+
// which would leave an observer on a detached node dead.
900+
observeMutations(document.body, scheduleProcess);
895901
scheduleProcess();
896902
}
897903

@@ -945,11 +951,11 @@
945951
}
946952

947953
function add_button_xdcc_rocks() {
948-
const resultsRoot = document.querySelector('.results');
949-
if (!resultsRoot) return;
950-
951-
observeMutations(resultsRoot, function () {
952-
if (resultsRoot.querySelector('tr.font2_bg0_bg1')) {
954+
// Observe <body>: the results container may be replaced on re-render,
955+
// which would leave an observer on a detached node dead.
956+
observeMutations(document.body, function () {
957+
const resultsRoot = document.querySelector('.results');
958+
if (resultsRoot && resultsRoot.querySelector('tr.font2_bg0_bg1')) {
953959
processRocksTable();
954960
}
955961
});
@@ -992,7 +998,8 @@
992998
}
993999

9941000
function processRocksTable() {
995-
const table = resultsRoot.querySelector('table');
1001+
const resultsRoot = document.querySelector('.results');
1002+
const table = resultsRoot ? resultsRoot.querySelector('table') : null;
9961003
if (!table) return;
9971004

9981005
let currentServer = null;
@@ -1107,8 +1114,9 @@
11071114

11081115
processCards();
11091116

1110-
const resultsContainer = document.querySelector(".results-container");
1111-
observeMutations(resultsContainer || document.body, processCards);
1117+
// Observe <body>: the results container may be replaced on re-render,
1118+
// which would leave an observer on a detached node dead.
1119+
observeMutations(document.body, processCards);
11121120
}
11131121

11141122
function add_button_xdcc_info() {
@@ -1129,8 +1137,10 @@
11291137

11301138
processInfoRows();
11311139

1132-
const table = document.querySelector('table.pack-table');
1133-
observeMutations((table && table.parentElement) || document.body, processInfoRows);
1140+
// Observe <body>: the pack table is replaced when filters (e.g.
1141+
// language) re-render, which would leave an observer on a detached
1142+
// node dead.
1143+
observeMutations(document.body, processInfoRows);
11341144
}
11351145

11361146
function add_button_skullxdcc() {
@@ -1170,8 +1180,9 @@
11701180
}
11711181

11721182
processSkullRows();
1173-
const wrapper = document.querySelector('.results-wrapper');
1174-
observeMutations(wrapper || document.body, processSkullRows);
1183+
// Observe <body>: the results wrapper may be replaced on re-render,
1184+
// which would leave an observer on a detached node dead.
1185+
observeMutations(document.body, processSkullRows);
11751186
}
11761187

11771188
const hostHandlers = {

0 commit comments

Comments
 (0)