Skip to content

Commit 73a5c5e

Browse files
committed
refactor: enhance animation reflow handling and improve changelog consistency checks
1 parent 344f2a5 commit 73a5c5e

8 files changed

Lines changed: 200 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ direkt zu einer versionierten Release-Sektion.
2121

2222
### Fixed
2323

24+
- Nutzerwirkung: Keine beabsichtigte sichtbare Verhaltensänderung; ausgewählte Board-, Sweep- und Trend-Animationen sowie der interne Changelog-Check laufen auf denselben fachlichen Pfaden weiter, sind aber in dieser Sonar-Welle klarer und direkter ausgedrückt.
25+
Technik: Die vier `void`-basierten Reflow-Trigger in `average-trend-arrow`, `turn-start-sweep` und `cricket-grid-fx` verwenden jetzt explizite lokale Reflow-Helfer statt des `void`-Operators, das kleine `S3776` in `scripts/check-changelog-consistency.mjs` wurde nur durch lokale Helper-Aufteilung reduziert, und neue direkte Runtime-Tests sichern die Re-Trigger-/Cleanup-Verträge der Sweep- und Trend-Animationen auf dem Fake-DOM gegen Drift ab.
26+
27+
- Nutzerwirkung: Keine beabsichtigte sichtbare Verhaltensänderung; die Board-SVG-Erkennung verarbeitet `viewBox`-Werte weiterhin identisch, drückt eine rein mechanische String-Normalisierung aber direkter aus.
28+
Technik: In `dartboard-svg` wurde ein einzelner Sonar-Reliability-Treffer (`S7781`) mit dem kleinstmöglichen Diff bereinigt, indem die feste Komma-Ersetzung von einer Regex-Variante auf `replaceAll(\",\", \" \")` umgestellt wurde; die bestehende Runtime-Abdeckung für ViewBox-Parsing und Board-Erkennung bleibt dabei unverändert der fachliche Schutz.
29+
2430
- Nutzerwirkung: Keine beabsichtigte sichtbare Verhaltensänderung; die nächste sichere Sonar-Welle hält `Triple/Double/Bull Hits` bei denselben Throw-, Burst- und Korrekturpfaden, räumt interne Dekorations-Metadaten aber konsequent auf moderneren DOM-Zugriff um.
2531
Technik: In `triple-double-bull-hits/logic` wurden die verbliebenen mechanischen `dataset`- und Optional-Chaining-Treffer lokal ersetzt, ohne Signaturen oder Ablaufsteuerung zu ändern; die bestehenden Runtime-Regressionen prüfen jetzt zusätzlich, dass Theme-/Burst-Metadaten beim Setzen und Löschen der Hit-Dekoration weiterhin korrekt über `dataset` und Attributspiegelung synchron bleiben.
2632

dist/autodarts-xconfig.user.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7191,7 +7191,7 @@
71917191
hasExactBoardViewBox: false
71927192
};
71937193
}
7194-
const rawViewBox = String(node.getAttribute("viewBox") || "").trim().replaceAll(/,/g, " ").replaceAll(/\s+/g, " ");
7194+
const rawViewBox = String(node.getAttribute("viewBox") || "").trim().replaceAll(",", " ").replaceAll(/\s+/g, " ");
71957195
if (!rawViewBox) {
71967196
return {
71977197
hasSquareViewBox: false,
@@ -11095,12 +11095,21 @@
1109511095
}
1109611096
return arrow;
1109711097
}
11098+
function forceAnimationReflow(node) {
11099+
if (!node) {
11100+
return 0;
11101+
}
11102+
if (Number.isFinite(node.offsetWidth)) {
11103+
return node.offsetWidth;
11104+
}
11105+
return Number(node.getBoundingClientRect?.().width || 0);
11106+
}
1109811107
function animateArrowNode(arrowNode, durationMs, timeoutByArrow) {
1109911108
if (!arrowNode?.classList) {
1110011109
return;
1110111110
}
1110211111
arrowNode.classList.remove(ANIMATE_CLASS);
11103-
void arrowNode.offsetWidth;
11112+
forceAnimationReflow(arrowNode);
1110411113
arrowNode.classList.add(ANIMATE_CLASS);
1110511114
const previousTimeout = timeoutByArrow.get(arrowNode);
1110611115
if (previousTimeout) {
@@ -11462,6 +11471,15 @@
1146211471
function findActivePlayerNode(documentRef) {
1146311472
return findBySelectors(documentRef, ACTIVE_PLAYER_SELECTORS);
1146411473
}
11474+
function forceAnimationReflow2(node) {
11475+
if (!node) {
11476+
return 0;
11477+
}
11478+
if (Number.isFinite(node.offsetWidth)) {
11479+
return node.offsetWidth;
11480+
}
11481+
return Number(node.getBoundingClientRect?.().width || 0);
11482+
}
1146511483
function runTurnStartSweep(node, state, config = {}, windowRef = null) {
1146611484
if (!node?.classList || !state) {
1146711485
return;
@@ -11471,7 +11489,7 @@
1147111489
const setTimeoutRef = windowRef && typeof windowRef.setTimeout === "function" ? windowRef.setTimeout.bind(windowRef) : setTimeout;
1147211490
const clearTimeoutRef = windowRef && typeof windowRef.clearTimeout === "function" ? windowRef.clearTimeout.bind(windowRef) : clearTimeout;
1147311491
node.classList.remove(SWEEP_CLASS);
11474-
void node.offsetWidth;
11492+
forceAnimationReflow2(node);
1147511493
node.classList.add(SWEEP_CLASS);
1147611494
state.nodes.add(node);
1147711495
const previousTimeout = state.timeoutsByNode.get(node);
@@ -19705,12 +19723,21 @@
1970519723
}
1970619724
badgeNode.classList.add(BADGE_STATE_CLASS.neutral);
1970719725
}
19726+
function forceAnimationReflow3(node) {
19727+
if (!node) {
19728+
return 0;
19729+
}
19730+
if (Number.isFinite(node.offsetWidth)) {
19731+
return node.offsetWidth;
19732+
}
19733+
return Number(node.getBoundingClientRect?.().width || 0);
19734+
}
1970819735
function toggleTimedClass(state, node, className, timeoutMs = 700) {
1970919736
if (!state || !node?.classList || !className) {
1971019737
return;
1971119738
}
1971219739
node.classList.remove(className);
19713-
void node.offsetWidth;
19740+
forceAnimationReflow3(node);
1971419741
node.classList.add(className);
1971519742
const timeoutRef = state.windowRef && typeof state.windowRef.setTimeout === "function" ? state.windowRef.setTimeout.bind(state.windowRef) : setTimeout;
1971619743
const handle = timeoutRef(() => {
@@ -19736,7 +19763,7 @@
1973619763
return;
1973719764
}
1973819765
clearProgressClasses(targetNode);
19739-
void targetNode.offsetWidth;
19766+
forceAnimationReflow3(targetNode);
1974019767
targetNode.classList.add(MARK_PROGRESS_CLASS);
1974119768
const clampedMarks = Math.max(0, Math.min(3, Number(marks) || 0));
1974219769
targetNode.classList.add(

scripts/check-changelog-consistency.mjs

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -109,65 +109,86 @@ function parseCompareLink(link) {
109109
};
110110
}
111111

112-
function validateSectionEntries(section) {
113-
const errors = [];
114-
const lines = section.body.split("\n");
115-
let entryCount = 0;
116-
let currentEntryState = null;
112+
function isSectionSubheadingLine(line) {
113+
return /^###\s+/.test(line);
114+
}
117115

118-
for (let index = 0; index < lines.length; index += 1) {
119-
const line = lines[index];
120-
if (!line.trim()) {
121-
continue;
122-
}
116+
function isIndentedTechnikLine(line) {
117+
return /^\s+Technik:\s+\S/.test(line);
118+
}
123119

124-
if (/^###\s+/.test(line)) {
125-
currentEntryState = null;
126-
continue;
127-
}
120+
function isIndentedNonTechnikContentLine(line) {
121+
return /^\s{2,}\S/.test(line) && !isIndentedTechnikLine(line);
122+
}
128123

129-
if (/^\s{2,}\S/.test(line) && !/^\s+Technik:\s+\S/.test(line)) {
130-
if (!currentEntryState) {
131-
errors.push(
132-
`Abschnitt ${section.name}: Eingerückter Fließtext muss zu einem Nutzerwirkung-/Technik-Eintrag gehören.`
133-
);
134-
}
135-
continue;
136-
}
124+
function isStartedUserImpactLine(line) {
125+
return /^- Nutzerwirkung:\s+\S/.test(line);
126+
}
127+
128+
function validateSectionEntryLine(sectionName, line, currentEntryState, errors) {
129+
if (!line.trim()) {
130+
return currentEntryState;
131+
}
137132

138-
const hasUserImpactPrefix = line.startsWith("- Nutzerwirkung: ");
139-
if (
140-
line.startsWith("- ") &&
141-
(!hasUserImpactPrefix || !line.slice("- Nutzerwirkung: ".length).trim())
142-
) {
143-
errors.push(`Abschnitt ${section.name}: Listenpunkte müssen mit "Nutzerwirkung:" beginnen.`);
133+
if (isSectionSubheadingLine(line)) {
134+
return null;
135+
}
136+
137+
if (isIndentedNonTechnikContentLine(line)) {
138+
if (!currentEntryState) {
139+
errors.push(
140+
`Abschnitt ${sectionName}: Eingerückter Fließtext muss zu einem Nutzerwirkung-/Technik-Eintrag gehören.`
141+
);
144142
}
143+
return currentEntryState;
144+
}
145145

146-
if (/^Technik:\s+\S/.test(line)) {
146+
if (line.startsWith("- ") && !isStartedUserImpactLine(line)) {
147+
errors.push(`Abschnitt ${sectionName}: Listenpunkte müssen mit "Nutzerwirkung:" beginnen.`);
148+
}
149+
150+
if (/^Technik:\s+\S/.test(line)) {
151+
errors.push(
152+
`Abschnitt ${sectionName}: "Technik:" muss eingerückt direkt unter "Nutzerwirkung:" stehen.`
153+
);
154+
}
155+
156+
if (isStartedUserImpactLine(line)) {
157+
if (currentEntryState === "user") {
147158
errors.push(
148-
`Abschnitt ${section.name}: "Technik:" muss eingerückt direkt unter "Nutzerwirkung:" stehen.`
159+
`Abschnitt ${sectionName}: Ein "Nutzerwirkung:"-Eintrag wurde begonnen, aber der zugehörige "Technik:"-Teil fehlt noch.`
149160
);
150161
}
162+
return "user";
163+
}
164+
165+
if (!isIndentedTechnikLine(line)) {
166+
return currentEntryState;
167+
}
168+
169+
if (currentEntryState !== "user") {
170+
errors.push(
171+
`Abschnitt ${sectionName}: "Technik:" darf nur direkt auf einen "Nutzerwirkung:"-Eintrag folgen.`
172+
);
173+
return currentEntryState;
174+
}
175+
176+
return "tech";
177+
}
178+
179+
function validateSectionEntries(section) {
180+
const errors = [];
181+
const lines = section.body.split("\n");
182+
let entryCount = 0;
183+
let currentEntryState = null;
151184

152-
if (/^- Nutzerwirkung:\s+\S/.test(line)) {
153-
if (currentEntryState === "user") {
154-
errors.push(
155-
`Abschnitt ${section.name}: Ein "Nutzerwirkung:"-Eintrag wurde begonnen, aber der zugehörige "Technik:"-Teil fehlt noch.`
156-
);
157-
}
185+
for (let index = 0; index < lines.length; index += 1) {
186+
const line = lines[index];
187+
if (isStartedUserImpactLine(line)) {
158188
entryCount += 1;
159-
currentEntryState = "user";
160189
}
161190

162-
if (/^\s+Technik:\s+\S/.test(line)) {
163-
if (currentEntryState !== "user") {
164-
errors.push(
165-
`Abschnitt ${section.name}: "Technik:" darf nur direkt auf einen "Nutzerwirkung:"-Eintrag folgen.`
166-
);
167-
} else {
168-
currentEntryState = "tech";
169-
}
170-
}
191+
currentEntryState = validateSectionEntryLine(section.name, line, currentEntryState, errors);
171192
}
172193

173194
if (currentEntryState === "user") {

src/features/average-trend-arrow/logic.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,25 @@ export function ensureArrowNode(avgNode, arrowByAverageNode, arrowNodes = null)
5858
return arrow;
5959
}
6060

61+
function forceAnimationReflow(node) {
62+
if (!node) {
63+
return 0;
64+
}
65+
66+
if (Number.isFinite(node.offsetWidth)) {
67+
return node.offsetWidth;
68+
}
69+
70+
return Number(node.getBoundingClientRect?.().width || 0);
71+
}
72+
6173
export function animateArrowNode(arrowNode, durationMs, timeoutByArrow) {
6274
if (!arrowNode?.classList) {
6375
return;
6476
}
6577

6678
arrowNode.classList.remove(ANIMATE_CLASS);
67-
void arrowNode.offsetWidth;
79+
forceAnimationReflow(arrowNode);
6880
arrowNode.classList.add(ANIMATE_CLASS);
6981

7082
const previousTimeout = timeoutByArrow.get(arrowNode);

src/features/cricket-grid-fx/logic.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,13 +557,25 @@ function setBadgeStateClasses(badgeNode, stateToken) {
557557
badgeNode.classList.add(BADGE_STATE_CLASS.neutral);
558558
}
559559

560+
function forceAnimationReflow(node) {
561+
if (!node) {
562+
return 0;
563+
}
564+
565+
if (Number.isFinite(node.offsetWidth)) {
566+
return node.offsetWidth;
567+
}
568+
569+
return Number(node.getBoundingClientRect?.().width || 0);
570+
}
571+
560572
function toggleTimedClass(state, node, className, timeoutMs = 700) {
561573
if (!state || !node?.classList || !className) {
562574
return;
563575
}
564576

565577
node.classList.remove(className);
566-
void node.offsetWidth;
578+
forceAnimationReflow(node);
567579
node.classList.add(className);
568580

569581
const timeoutRef =
@@ -602,7 +614,7 @@ function triggerMarkProgress(state, cellNode, marks, visualConfig) {
602614
}
603615

604616
clearProgressClasses(targetNode);
605-
void targetNode.offsetWidth;
617+
forceAnimationReflow(targetNode);
606618
targetNode.classList.add(MARK_PROGRESS_CLASS);
607619
const clampedMarks = Math.max(0, Math.min(3, Number(marks) || 0));
608620
targetNode.classList.add(

src/features/turn-start-sweep/logic.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export function findActivePlayerNode(documentRef) {
2424
return findBySelectors(documentRef, ACTIVE_PLAYER_SELECTORS);
2525
}
2626

27+
function forceAnimationReflow(node) {
28+
if (!node) {
29+
return 0;
30+
}
31+
32+
if (Number.isFinite(node.offsetWidth)) {
33+
return node.offsetWidth;
34+
}
35+
36+
return Number(node.getBoundingClientRect?.().width || 0);
37+
}
38+
2739
export function runTurnStartSweep(node, state, config = {}, windowRef = null) {
2840
if (!node?.classList || !state) {
2941
return;
@@ -42,7 +54,7 @@ export function runTurnStartSweep(node, state, config = {}, windowRef = null) {
4254

4355
node.classList.remove(SWEEP_CLASS);
4456
// Force style recalculation so class re-apply retriggers the keyframes.
45-
void node.offsetWidth;
57+
forceAnimationReflow(node);
4658
node.classList.add(SWEEP_CLASS);
4759
state.nodes.add(node);
4860

src/shared/dartboard-svg.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ function parseViewBoxMetrics(node) {
373373

374374
const rawViewBox = String(node.getAttribute("viewBox") || "")
375375
.trim()
376-
.replaceAll(/,/g, " ")
376+
.replaceAll(",", " ")
377377
.replaceAll(/\s+/g, " ");
378378
if (!rawViewBox) {
379379
return {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import test from "node:test";
2+
import assert from "node:assert/strict";
3+
4+
import { animateArrowNode } from "../../src/features/average-trend-arrow/logic.js";
5+
import { ANIMATE_CLASS } from "../../src/features/average-trend-arrow/style.js";
6+
import { runTurnStartSweep } from "../../src/features/turn-start-sweep/logic.js";
7+
import { SWEEP_CLASS } from "../../src/features/turn-start-sweep/style.js";
8+
import { FakeDocument, createFakeWindow } from "./fake-dom.js";
9+
10+
function wait(ms = 0) {
11+
return new Promise((resolve) => setTimeout(resolve, ms));
12+
}
13+
14+
function createImmediateWindow(documentRef) {
15+
const windowRef = createFakeWindow({ documentRef });
16+
const nativeSetTimeout = setTimeout;
17+
windowRef.setTimeout = (callback, _ms, ...args) => nativeSetTimeout(callback, 0, ...args);
18+
windowRef.clearTimeout = (handle) => clearTimeout(handle);
19+
return windowRef;
20+
}
21+
22+
test("animateArrowNode retriggers the animation class on fake-dom nodes without offsetWidth", async () => {
23+
const documentRef = new FakeDocument();
24+
const arrowNode = documentRef.createElement("span");
25+
const timeoutByArrow = new Map();
26+
27+
animateArrowNode(arrowNode, 320, timeoutByArrow);
28+
29+
assert.equal(arrowNode.classList.contains(ANIMATE_CLASS), true);
30+
assert.equal(timeoutByArrow.has(arrowNode), true);
31+
clearTimeout(timeoutByArrow.get(arrowNode));
32+
});
33+
34+
test("runTurnStartSweep retriggers and clears the sweep class on fake-dom nodes without offsetWidth", async () => {
35+
const documentRef = new FakeDocument();
36+
const windowRef = createImmediateWindow(documentRef);
37+
const node = documentRef.createElement("div");
38+
const state = {
39+
nodes: new Set(),
40+
timeoutsByNode: new Map(),
41+
};
42+
43+
runTurnStartSweep(node, state, { durationMs: 420, sweepDelayMs: 0 }, windowRef);
44+
45+
assert.equal(node.classList.contains(SWEEP_CLASS), true);
46+
assert.equal(state.nodes.has(node), true);
47+
assert.equal(state.timeoutsByNode.has(node), true);
48+
49+
await wait(10);
50+
51+
assert.equal(node.classList.contains(SWEEP_CLASS), false);
52+
assert.equal(state.nodes.has(node), false);
53+
assert.equal(state.timeoutsByNode.has(node), false);
54+
});

0 commit comments

Comments
 (0)