Skip to content

Commit 6ec4664

Browse files
committed
perf(turn-points-count): scope observer to turn surface
- observe #ad-ext-turn directly when available - preserve fallback behavior without Tools-for-Autodarts roots - keep animation self-mutation filtering intact
1 parent 71f081f commit 6ec4664

2 files changed

Lines changed: 316 additions & 4 deletions

File tree

src/features/turn-points-count/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
updateTurnPoints,
1010
} from "./logic.js";
1111
import { STYLE_ID, buildStyleText } from "./style.js";
12-
import { createTurnSurfaceObserveOptions } from "../shared/turn-surface-adapter.js";
12+
import {
13+
createTurnSurfaceObserveOptions,
14+
findTurnContainer,
15+
} from "../shared/turn-surface-adapter.js";
1316

1417
const FEATURE_KEY = "turn-points-count";
1518
const OBSERVER_KEY = `${FEATURE_KEY}:dom-observer`;
@@ -74,7 +77,11 @@ export function initializeTurnPointsCount(context = {}) {
7477
}
7578

7679
const scheduler = schedulerFactory(update, { windowRef });
77-
const rootNode = documentRef.documentElement || documentRef.body || documentRef;
80+
const rootNode =
81+
findTurnContainer(documentRef) ||
82+
documentRef.documentElement ||
83+
documentRef.body ||
84+
documentRef;
7885
const isAnimatingScoreNode = (node) => {
7986
const candidate = node?.nodeType === 3 ? node.parentNode || null : node;
8087
return (

tests/runtime/turn-points-count.test.js

Lines changed: 307 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
stopAnimation,
66
updateTurnPoints,
77
} from "../../src/features/turn-points-count/logic.js";
8+
import { initializeTurnPointsCount } from "../../src/features/turn-points-count/index.js";
89
import {
910
SCORE_FRAME_CLASS,
1011
SCORE_FLASH_SEQUENCE_ATTRIBUTE,
@@ -49,18 +50,151 @@ function createTurnPointsFrame(documentRef) {
4950

5051
function createAnimeStub() {
5152
const calls = [];
53+
const instances = [];
5254

5355
const anime = (params = {}) => {
5456
calls.push(params);
55-
return {
56-
pause() {},
57+
const instance = {
58+
paused: false,
59+
pause() {
60+
this.paused = true;
61+
},
5762
};
63+
instances.push(instance);
64+
return instance;
5865
};
5966

6067
anime.calls = calls;
68+
anime.instances = instances;
6169
return anime;
6270
}
6371

72+
function createObserverRegistryProbe() {
73+
const state = {
74+
registration: null,
75+
disconnects: [],
76+
};
77+
78+
return {
79+
state,
80+
registry: {
81+
registerMutationObserver(options = {}) {
82+
state.registration = options;
83+
return {};
84+
},
85+
disconnect(key) {
86+
state.disconnects.push(String(key || ""));
87+
return true;
88+
},
89+
},
90+
};
91+
}
92+
93+
function createListenerRegistryProbe() {
94+
const state = {
95+
registrations: [],
96+
removals: [],
97+
};
98+
99+
return {
100+
state,
101+
registry: {
102+
register(options = {}) {
103+
state.registrations.push(options);
104+
},
105+
remove(key) {
106+
state.removals.push(String(key || ""));
107+
},
108+
},
109+
};
110+
}
111+
112+
function createImmediateSchedulerFactory(scheduleCounter) {
113+
return function createImmediateScheduler(callback) {
114+
let cancelled = false;
115+
116+
return {
117+
schedule() {
118+
if (cancelled) {
119+
return;
120+
}
121+
scheduleCounter.count += 1;
122+
callback();
123+
},
124+
cancel() {
125+
cancelled = true;
126+
scheduleCounter.cancelled = true;
127+
},
128+
isScheduled() {
129+
return false;
130+
},
131+
};
132+
};
133+
}
134+
135+
function createMountHarness(options = {}) {
136+
const documentRef = options.documentRef || new FakeDocument();
137+
const windowRef = options.windowRef || createFakeWindow({ documentRef });
138+
const animeRef = options.animeRef || createAnimeStub();
139+
windowRef.anime = animeRef;
140+
const observerProbe = createObserverRegistryProbe();
141+
const listenerProbe = createListenerRegistryProbe();
142+
const scheduleCounter = {
143+
count: 0,
144+
cancelled: false,
145+
};
146+
let unsubscribeCount = 0;
147+
let gameStateListener = null;
148+
149+
const cleanup = initializeTurnPointsCount({
150+
documentRef,
151+
windowRef,
152+
registries: {
153+
observers: observerProbe.registry,
154+
listeners: listenerProbe.registry,
155+
},
156+
gameState: {
157+
subscribe(listener) {
158+
gameStateListener = listener;
159+
return () => {
160+
unsubscribeCount += 1;
161+
};
162+
},
163+
},
164+
config: {
165+
getFeatureConfig() {
166+
return {
167+
durationMs: 416,
168+
flashOnChange: true,
169+
flashMode: "on-change",
170+
};
171+
},
172+
},
173+
helpers: {
174+
createRafScheduler: createImmediateSchedulerFactory(scheduleCounter),
175+
},
176+
});
177+
178+
return {
179+
animeRef,
180+
cleanup,
181+
documentRef,
182+
gameStateListener,
183+
listenerProbe,
184+
observerProbe,
185+
scheduleCounter,
186+
windowRef,
187+
get unsubscribeCount() {
188+
return unsubscribeCount;
189+
},
190+
};
191+
}
192+
193+
function moveTurnPointsIntoTurnContainer(documentRef) {
194+
documentRef.turnContainer.appendChild(documentRef.turnPointsElement);
195+
return createTurnPointsFrame(documentRef);
196+
}
197+
64198
test("turn-points-count keeps flash frame for a short afterglow after score animation completes", async () => {
65199
const documentRef = new FakeDocument();
66200
const windowRef = createFakeWindow({ documentRef });
@@ -351,6 +485,177 @@ test("turn-points-count restarts score flash through sequence attributes without
351485
assert.equal(frameNode.classList.contains(SCORE_FRAME_CLASS), true);
352486
});
353487

488+
test("turn-points-count observes the discovered turn surface when present", () => {
489+
const harness = createMountHarness();
490+
491+
assert.equal(
492+
harness.observerProbe.state.registration?.target,
493+
harness.documentRef.turnContainer
494+
);
495+
496+
harness.cleanup();
497+
});
498+
499+
test("turn-points-count falls back to the document root when turn surface is absent", () => {
500+
const documentRef = new FakeDocument();
501+
documentRef.turnContainer.remove();
502+
const harness = createMountHarness({ documentRef });
503+
504+
assert.equal(
505+
harness.observerProbe.state.registration?.target,
506+
documentRef.documentElement
507+
);
508+
509+
harness.cleanup();
510+
});
511+
512+
test("turn-points-count does not schedule for unrelated document mutations when scoped", () => {
513+
const harness = createMountHarness();
514+
const callback = harness.observerProbe.state.registration?.callback;
515+
const initialScheduleCount = harness.scheduleCounter.count;
516+
const unrelatedNode = harness.documentRef.createElement("div");
517+
harness.documentRef.body.appendChild(unrelatedNode);
518+
519+
callback([
520+
{
521+
type: "childList",
522+
target: harness.documentRef.body,
523+
addedNodes: [unrelatedNode],
524+
removedNodes: [],
525+
},
526+
]);
527+
528+
assert.equal(harness.observerProbe.state.registration?.target, harness.documentRef.turnContainer);
529+
assert.equal(harness.scheduleCounter.count, initialScheduleCount);
530+
531+
harness.cleanup();
532+
});
533+
534+
test("turn-points-count schedules relevant turn text, child, and class mutations", () => {
535+
const documentRef = new FakeDocument();
536+
const { scoreNode } = moveTurnPointsIntoTurnContainer(documentRef);
537+
const harness = createMountHarness({ documentRef });
538+
const callback = harness.observerProbe.state.registration?.callback;
539+
const textNode = {
540+
nodeType: 3,
541+
parentNode: scoreNode,
542+
};
543+
const addedNode = documentRef.createElement("div");
544+
let expectedScheduleCount = harness.scheduleCounter.count;
545+
546+
callback([
547+
{
548+
type: "characterData",
549+
target: textNode,
550+
},
551+
]);
552+
expectedScheduleCount += 1;
553+
assert.equal(harness.scheduleCounter.count, expectedScheduleCount);
554+
555+
callback([
556+
{
557+
type: "childList",
558+
target: documentRef.turnContainer,
559+
addedNodes: [addedNode],
560+
removedNodes: [],
561+
},
562+
]);
563+
expectedScheduleCount += 1;
564+
assert.equal(harness.scheduleCounter.count, expectedScheduleCount);
565+
566+
callback([
567+
{
568+
type: "attributes",
569+
attributeName: "class",
570+
target: documentRef.throwRow,
571+
},
572+
]);
573+
expectedScheduleCount += 1;
574+
assert.equal(harness.scheduleCounter.count, expectedScheduleCount);
575+
576+
harness.cleanup();
577+
});
578+
579+
test("turn-points-count ignores self-generated animation mutations", () => {
580+
const documentRef = new FakeDocument();
581+
const { scoreNode, frameNode } = moveTurnPointsIntoTurnContainer(documentRef);
582+
const harness = createMountHarness({ documentRef });
583+
const callback = harness.observerProbe.state.registration?.callback;
584+
const textNode = {
585+
nodeType: 3,
586+
parentNode: scoreNode,
587+
};
588+
589+
scoreNode.textContent = "45";
590+
callback([
591+
{
592+
type: "characterData",
593+
target: textNode,
594+
},
595+
]);
596+
597+
assert.equal(harness.animeRef.calls.length, 1);
598+
const scheduleCountAfterAnimation = harness.scheduleCounter.count;
599+
600+
callback([
601+
{
602+
type: "characterData",
603+
target: textNode,
604+
},
605+
]);
606+
callback([
607+
{
608+
type: "attributes",
609+
attributeName: "class",
610+
target: scoreNode,
611+
},
612+
{
613+
type: "attributes",
614+
attributeName: "class",
615+
target: frameNode,
616+
},
617+
]);
618+
619+
assert.equal(scoreNode.classList.contains(SCORE_FLASH_CLASS), true);
620+
assert.equal(frameNode.classList.contains(SCORE_FRAME_CLASS), true);
621+
assert.equal(harness.scheduleCounter.count, scheduleCountAfterAnimation);
622+
623+
harness.cleanup();
624+
});
625+
626+
test("turn-points-count cleanup disconnects observer and stops active animations", () => {
627+
const documentRef = new FakeDocument();
628+
const { scoreNode, frameNode } = moveTurnPointsIntoTurnContainer(documentRef);
629+
const harness = createMountHarness({ documentRef });
630+
const callback = harness.observerProbe.state.registration?.callback;
631+
const textNode = {
632+
nodeType: 3,
633+
parentNode: scoreNode,
634+
};
635+
636+
scoreNode.textContent = "45";
637+
callback([
638+
{
639+
type: "characterData",
640+
target: textNode,
641+
},
642+
]);
643+
644+
assert.equal(harness.animeRef.instances[0]?.paused, false);
645+
assert.equal(scoreNode.classList.contains(SCORE_FLASH_CLASS), true);
646+
assert.equal(frameNode.classList.contains(SCORE_FRAME_CLASS), true);
647+
648+
harness.cleanup();
649+
650+
assert.deepEqual(harness.observerProbe.state.disconnects, ["turn-points-count:dom-observer"]);
651+
assert.equal(harness.listenerProbe.state.removals.includes("turn-points-count:document-visibility"), true);
652+
assert.equal(harness.unsubscribeCount, 1);
653+
assert.equal(harness.scheduleCounter.cancelled, true);
654+
assert.equal(harness.animeRef.instances[0]?.paused, true);
655+
assert.equal(scoreNode.classList.contains(SCORE_FLASH_CLASS), false);
656+
assert.equal(frameNode.classList.contains(SCORE_FRAME_CLASS), false);
657+
});
658+
354659
test("turn-points-count style exports the scoped flash animation contract", () => {
355660
const css = buildStyleText();
356661

0 commit comments

Comments
 (0)