@four-leaf-studios/rl-overlay 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +70 -0
- package/dist/index.cjs.js +298 -10599
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +328 -10612
- package/dist/index.esm.js.map +1 -1
- package/dist/types/Overlay.d.ts +5 -6
- package/dist/types/OverlaySlot.d.ts +9 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/registry.d.ts +2 -0
- package/package.json +12 -5
- package/src/Overlay.tsx +47 -41
- package/src/OverlaySlot.tsx +35 -0
- package/src/Player.tsx +1 -0
- package/src/PlayerBoost.tsx +4 -1
- package/src/Scoreboard.tsx +1 -0
- package/src/ScoreboardGameBox.tsx +2 -4
- package/src/ScoreboardSeriesBox.tsx +2 -3
- package/src/ScoreboardTeam.tsx +1 -0
- package/src/StatItem.tsx +4 -1
- package/src/TargetBoost.tsx +1 -0
- package/src/TargetPlayer.tsx +1 -0
- package/src/TargetPlayerLocation.tsx +1 -0
- package/src/TargetPlayerStats.tsx +1 -1
- package/src/Team.tsx +1 -0
- package/src/Timer.tsx +7 -4
- package/src/css/Florida_Tech.css +696 -0
- package/src/css/Horizon_Hues.css +618 -0
- package/src/css/Neo_Glass.css +674 -0
- package/src/css/Obsidian_Glide.css +646 -0
- package/src/css/RLCS_2024.css +566 -0
- package/src/css/RLCS_2025.css +524 -0
- package/src/css/Shaded_blocks.css +594 -0
- package/src/index.ts +2 -0
- package/src/registry.ts +19 -0
- package/src/types.d.ts +20 -0
- package/test-overlay/package-lock.json +2697 -0
- package/test-overlay/package.json +3 -0
- package/test-overlay/public/mock-css.css +259 -386
- package/test-overlay/src/App.jsx +78 -28
- package/tests/BroadcastContext.test.tsx +41 -0
- package/tests/Overlay.test.tsx +106 -0
- package/tests/OverlaySlot.test.tsx +79 -0
- package/tests/PlayerBoost.test.tsx +47 -0
- package/tests/Replay.test.tsx +29 -0
- package/tests/ScoreboardGameBox.test.tsx +35 -0
- package/tests/ScoreboardSeriesBox.test.tsx +48 -0
- package/tests/StatItem.test.tsx +33 -0
- package/tests/__mocks__/@four-leaf-studios/rl-socket-hook.ts +10 -0
- package/tests/fixtures.ts +96 -0
- package/tests/registry.test.ts +27 -0
- package/tests/setup.ts +1 -0
- package/tsconfig.json +16 -20
- package/vitest.config.ts +9 -0
package/test-overlay/src/App.jsx
CHANGED
|
@@ -1,28 +1,78 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Overlay } from "@four-leaf-studios/rl-overlay";
|
|
3
|
-
import { mockBroadcastData } from "./mockBroadcast";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Overlay } from "@four-leaf-studios/rl-overlay";
|
|
3
|
+
import { mockBroadcastData } from "./mockBroadcast";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build an overlay config from a CSS theme string.
|
|
7
|
+
* Each component in the registry gets its own entry.
|
|
8
|
+
* The CSS is applied to the first component since all CSS
|
|
9
|
+
* gets concatenated into a single <style> block anyway.
|
|
10
|
+
*/
|
|
11
|
+
const buildOverlay = (css) => ({
|
|
12
|
+
components: [
|
|
13
|
+
{
|
|
14
|
+
id: "scoreboard",
|
|
15
|
+
name: "Scoreboard",
|
|
16
|
+
css: css, // all theme CSS goes here
|
|
17
|
+
code_id: "Scoreboard",
|
|
18
|
+
position: { top: 20, left: 560, width: 800, height: 130 },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "teams",
|
|
22
|
+
name: "Teams",
|
|
23
|
+
css: "",
|
|
24
|
+
code_id: "Teams",
|
|
25
|
+
position: { top: 200, left: 0, width: 350, height: 700 },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "target-player",
|
|
29
|
+
name: "Target Player",
|
|
30
|
+
css: "",
|
|
31
|
+
code_id: "TargetPlayer",
|
|
32
|
+
position: { top: 850, left: 560, width: 800, height: 200 },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "target-boost",
|
|
36
|
+
name: "Target Boost",
|
|
37
|
+
css: "",
|
|
38
|
+
code_id: "TargetBoost",
|
|
39
|
+
position: { top: 750, left: 1700, width: 200, height: 200 },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "replay",
|
|
43
|
+
name: "Replay",
|
|
44
|
+
css: "",
|
|
45
|
+
code_id: "Replay",
|
|
46
|
+
position: { top: 20, left: 860, width: 200, height: 50 },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default function App() {
|
|
52
|
+
const [cssText, setCssText] = useState("");
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
fetch("/mock-css.css")
|
|
56
|
+
.then((res) => {
|
|
57
|
+
if (!res.ok) throw new Error("Failed to load CSS");
|
|
58
|
+
return res.text();
|
|
59
|
+
})
|
|
60
|
+
.then(setCssText)
|
|
61
|
+
.catch((err) => {
|
|
62
|
+
console.error(err);
|
|
63
|
+
setCssText("");
|
|
64
|
+
});
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
if (!cssText) {
|
|
68
|
+
return <div>Loading styles…</div>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Overlay
|
|
73
|
+
broadcast={mockBroadcastData}
|
|
74
|
+
overlay={buildOverlay(cssText)}
|
|
75
|
+
preview={true}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
BroadcastProvider,
|
|
6
|
+
useBroadcast,
|
|
7
|
+
} from "../src/context/BroadcastContext";
|
|
8
|
+
import { mockBroadcast } from "./fixtures";
|
|
9
|
+
|
|
10
|
+
describe("BroadcastContext", () => {
|
|
11
|
+
it("provides broadcast data to children", () => {
|
|
12
|
+
const TestConsumer = () => {
|
|
13
|
+
const broadcast = useBroadcast();
|
|
14
|
+
return <div data-testid="name">{broadcast.name}</div>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const { getByTestId } = render(
|
|
18
|
+
<BroadcastProvider broadcast={mockBroadcast}>
|
|
19
|
+
<TestConsumer />
|
|
20
|
+
</BroadcastProvider>,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(getByTestId("name").textContent).toBe("Test Broadcast");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("throws when useBroadcast is used outside provider", () => {
|
|
27
|
+
const TestConsumer = () => {
|
|
28
|
+
const broadcast = useBroadcast();
|
|
29
|
+
return <div>{broadcast.name}</div>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Suppress React error boundary noise
|
|
33
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
34
|
+
|
|
35
|
+
expect(() => render(<TestConsumer />)).toThrow(
|
|
36
|
+
"useBroadcast must be used within a BroadcastProvider",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
spy.mockRestore();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Overlay } from "../src/Overlay";
|
|
5
|
+
import { mockBroadcast, mockOverlay } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
vi.mock("@four-leaf-studios/rl-socket-hook", () => ({
|
|
8
|
+
useEventSelector: vi.fn(() => null),
|
|
9
|
+
useEvent: vi.fn(() => null),
|
|
10
|
+
RLProvider: ({ children }: { children: React.ReactNode }) =>
|
|
11
|
+
React.createElement("div", { "data-testid": "rl-provider" }, children),
|
|
12
|
+
WebsocketData: () =>
|
|
13
|
+
React.createElement("div", { "data-testid": "websocket-data" }),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("Overlay", () => {
|
|
17
|
+
it("renders overlay wrapper with components", () => {
|
|
18
|
+
const { container } = render(
|
|
19
|
+
<Overlay broadcast={mockBroadcast} overlay={mockOverlay} />,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(container.querySelector(".overlay-wrapper")).toBeInTheDocument();
|
|
23
|
+
expect(container.querySelector(".overlay")).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders overlay slots for each component", () => {
|
|
27
|
+
const { container } = render(
|
|
28
|
+
<Overlay broadcast={mockBroadcast} overlay={mockOverlay} />,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const slots = container.querySelectorAll(".overlay-slot");
|
|
32
|
+
expect(slots).toHaveLength(mockOverlay.components.length);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("injects CSS from components into a style tag", () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<Overlay broadcast={mockBroadcast} overlay={mockOverlay} />,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const style = container.querySelector("style");
|
|
41
|
+
expect(style).toBeInTheDocument();
|
|
42
|
+
expect(style?.textContent).toContain(".scoreboard_box");
|
|
43
|
+
expect(style?.textContent).toContain(".team_box");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("shows preview data panel when preview is true", () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<Overlay broadcast={mockBroadcast} overlay={mockOverlay} preview />,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(container.querySelector(".testing")).toBeInTheDocument();
|
|
52
|
+
expect(container.querySelector(".testing-data")).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("does not show preview panel when preview is false", () => {
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<Overlay broadcast={mockBroadcast} overlay={mockOverlay} />,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(container.querySelector(".testing-data")).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("shows missing component message for unknown code_id", () => {
|
|
64
|
+
const overlayWithBadComponent = {
|
|
65
|
+
components: [
|
|
66
|
+
{
|
|
67
|
+
id: "comp-bad",
|
|
68
|
+
name: "Unknown",
|
|
69
|
+
css: "",
|
|
70
|
+
code_id: "NonExistent",
|
|
71
|
+
position: { top: 0, left: 0 },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
render(
|
|
77
|
+
<Overlay broadcast={mockBroadcast} overlay={overlayWithBadComponent} />,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(
|
|
81
|
+
screen.getByText(/Missing component: NonExistent/),
|
|
82
|
+
).toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("uses renderSlot when provided", () => {
|
|
86
|
+
const renderSlot = vi.fn((comp, Comp) =>
|
|
87
|
+
React.createElement("div", {
|
|
88
|
+
key: comp.id,
|
|
89
|
+
"data-testid": "custom-slot",
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<Overlay
|
|
95
|
+
broadcast={mockBroadcast}
|
|
96
|
+
overlay={mockOverlay}
|
|
97
|
+
renderSlot={renderSlot}
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(renderSlot).toHaveBeenCalledTimes(mockOverlay.components.length);
|
|
102
|
+
expect(screen.getAllByTestId("custom-slot")).toHaveLength(
|
|
103
|
+
mockOverlay.components.length,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { OverlaySlot } from "../src/OverlaySlot";
|
|
5
|
+
|
|
6
|
+
describe("OverlaySlot", () => {
|
|
7
|
+
const baseComponent = {
|
|
8
|
+
id: "slot-1",
|
|
9
|
+
name: "TestSlot",
|
|
10
|
+
css: "",
|
|
11
|
+
code_id: "TestComponent",
|
|
12
|
+
position: { top: 50, left: 100, width: 200, height: 150 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it("renders children inside positioned container", () => {
|
|
16
|
+
const { container, getByText } = render(
|
|
17
|
+
<OverlaySlot component={baseComponent}>
|
|
18
|
+
<span>Content</span>
|
|
19
|
+
</OverlaySlot>,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(getByText("Content")).toBeInTheDocument();
|
|
23
|
+
const slot = container.querySelector(".overlay-slot");
|
|
24
|
+
expect(slot).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("applies position styles from component data", () => {
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<OverlaySlot component={baseComponent}>
|
|
30
|
+
<span>Content</span>
|
|
31
|
+
</OverlaySlot>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const slot = container.querySelector(".overlay-slot") as HTMLElement;
|
|
35
|
+
expect(slot.style.position).toBe("absolute");
|
|
36
|
+
expect(slot.style.top).toBe("50px");
|
|
37
|
+
expect(slot.style.left).toBe("100px");
|
|
38
|
+
expect(slot.style.width).toBe("200px");
|
|
39
|
+
expect(slot.style.height).toBe("150px");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses default position values when position is undefined", () => {
|
|
43
|
+
const componentNoPosition = { ...baseComponent, position: undefined };
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<OverlaySlot component={componentNoPosition}>
|
|
46
|
+
<span>Content</span>
|
|
47
|
+
</OverlaySlot>,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const slot = container.querySelector(".overlay-slot") as HTMLElement;
|
|
51
|
+
expect(slot.style.top).toBe("0px");
|
|
52
|
+
expect(slot.style.left).toBe("0px");
|
|
53
|
+
expect(slot.style.width).toBe("auto");
|
|
54
|
+
expect(slot.style.height).toBe("auto");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("sets data attributes from component", () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<OverlaySlot component={baseComponent}>
|
|
60
|
+
<span>Content</span>
|
|
61
|
+
</OverlaySlot>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const slot = container.querySelector(".overlay-slot");
|
|
65
|
+
expect(slot?.getAttribute("data-component-id")).toBe("slot-1");
|
|
66
|
+
expect(slot?.getAttribute("data-component-name")).toBe("TestSlot");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("passes extra div props through", () => {
|
|
70
|
+
const { container } = render(
|
|
71
|
+
<OverlaySlot component={baseComponent} data-custom="value">
|
|
72
|
+
<span>Content</span>
|
|
73
|
+
</OverlaySlot>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const slot = container.querySelector(".overlay-slot");
|
|
77
|
+
expect(slot?.getAttribute("data-custom")).toBe("value");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { PlayerBoost } from "../src/PlayerBoost";
|
|
5
|
+
|
|
6
|
+
vi.mock("framer-motion", () => ({
|
|
7
|
+
motion: {
|
|
8
|
+
div: ({ children, className, ...props }: any) =>
|
|
9
|
+
React.createElement("div", { className, ...props }, children),
|
|
10
|
+
},
|
|
11
|
+
AnimatePresence: ({ children }: any) => children,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
describe("PlayerBoost", () => {
|
|
15
|
+
it("renders boost meter for left team", () => {
|
|
16
|
+
const { container } = render(<PlayerBoost team={0} boost={50} />);
|
|
17
|
+
|
|
18
|
+
expect(container.querySelector(".boost_meter")).toBeInTheDocument();
|
|
19
|
+
expect(container.querySelector(".left_boost_meter")).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders boost meter for right team", () => {
|
|
23
|
+
const { container } = render(<PlayerBoost team={1} boost={50} />);
|
|
24
|
+
|
|
25
|
+
expect(container.querySelector(".right_boost_meter")).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("clamps boost to 0-100 range", () => {
|
|
29
|
+
const { container: containerOver } = render(
|
|
30
|
+
<PlayerBoost team={0} boost={150} />,
|
|
31
|
+
);
|
|
32
|
+
const barOver = containerOver.querySelector(
|
|
33
|
+
".boost_meter_bar",
|
|
34
|
+
) as HTMLElement;
|
|
35
|
+
// framer-motion is mocked, so we check the animate prop isn't applied
|
|
36
|
+
// but the component should clamp internally
|
|
37
|
+
expect(barOver).toBeInTheDocument();
|
|
38
|
+
|
|
39
|
+
const { container: containerUnder } = render(
|
|
40
|
+
<PlayerBoost team={0} boost={-50} />,
|
|
41
|
+
);
|
|
42
|
+
const barUnder = containerUnder.querySelector(
|
|
43
|
+
".boost_meter_bar",
|
|
44
|
+
) as HTMLElement;
|
|
45
|
+
expect(barUnder).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Replay } from "../src/Replay";
|
|
5
|
+
|
|
6
|
+
vi.mock("@four-leaf-studios/rl-socket-hook", () => ({
|
|
7
|
+
useEventSelector: vi.fn(() => null),
|
|
8
|
+
useEvent: vi.fn(() => null),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// We need to mock useReplay since it depends on the socket hook
|
|
12
|
+
const mockUseReplay = vi.fn();
|
|
13
|
+
vi.mock("../src/hooks/useReplay", () => ({
|
|
14
|
+
default: () => mockUseReplay(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("Replay", () => {
|
|
18
|
+
it("renders replay box when replay is active", () => {
|
|
19
|
+
mockUseReplay.mockReturnValue({ active: true });
|
|
20
|
+
render(<Replay />);
|
|
21
|
+
expect(screen.getByText("Replay")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns null when replay is not active", () => {
|
|
25
|
+
mockUseReplay.mockReturnValue({ active: false });
|
|
26
|
+
const { container } = render(<Replay />);
|
|
27
|
+
expect(container.innerHTML).toBe("");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ScoreboardGameBox } from "../src/ScoreboardGameBox";
|
|
5
|
+
import { mockBroadcast } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
describe("ScoreboardGameBox", () => {
|
|
8
|
+
it("calculates game number from series scores", () => {
|
|
9
|
+
render(<ScoreboardGameBox broadcast={mockBroadcast} />);
|
|
10
|
+
|
|
11
|
+
// Blue has 2, Orange has 1, so game number = 2 + 1 + 1 = 4
|
|
12
|
+
expect(screen.getByText("Game 4 - BO 5")).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("shows game 1 when no series scores", () => {
|
|
16
|
+
const freshBroadcast = {
|
|
17
|
+
...mockBroadcast,
|
|
18
|
+
teams: mockBroadcast.teams.map((t) => ({ ...t, series_score: 0 })),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
render(<ScoreboardGameBox broadcast={freshBroadcast} />);
|
|
22
|
+
expect(screen.getByText("Game 1 - BO 5")).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("handles missing teams gracefully", () => {
|
|
26
|
+
const noTeamsBroadcast = {
|
|
27
|
+
...mockBroadcast,
|
|
28
|
+
teams: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
render(<ScoreboardGameBox broadcast={noTeamsBroadcast as any} />);
|
|
32
|
+
// With no teams, defaults to game 1
|
|
33
|
+
expect(screen.getByText("Game 1 - BO 5")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { ScoreboardSeriesBoxComponent } from "../src/ScoreboardSeriesBox";
|
|
5
|
+
import { mockBroadcast } from "./fixtures";
|
|
6
|
+
|
|
7
|
+
describe("ScoreboardSeriesBox", () => {
|
|
8
|
+
const team = mockBroadcast.teams[0]; // series_score: 2
|
|
9
|
+
|
|
10
|
+
it("renders correct number of score indicators for BO5", () => {
|
|
11
|
+
const { container } = render(
|
|
12
|
+
<ScoreboardSeriesBoxComponent team={team} seriesNumber={5} />,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// BO5 → ceil(5/2) = 3 indicators
|
|
16
|
+
const boxes = container.querySelectorAll(".series_score_box");
|
|
17
|
+
expect(boxes).toHaveLength(3);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("marks filled indicators based on series_score", () => {
|
|
21
|
+
const { container } = render(
|
|
22
|
+
<ScoreboardSeriesBoxComponent team={team} seriesNumber={5} />,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// team has series_score=2, so 2 filled, 1 empty
|
|
26
|
+
const filled = container.querySelectorAll(".left_series_score_box_point");
|
|
27
|
+
const empty = container.querySelectorAll(".left_series_score_box_empty");
|
|
28
|
+
expect(filled).toHaveLength(2);
|
|
29
|
+
expect(empty).toHaveLength(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns null when seriesNumber is 0", () => {
|
|
33
|
+
const { container } = render(
|
|
34
|
+
<ScoreboardSeriesBoxComponent team={team} seriesNumber={0} />,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(container.innerHTML).toBe("");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("applies correct modifier for right team", () => {
|
|
41
|
+
const rightTeam = mockBroadcast.teams[1]; // id: "1"
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<ScoreboardSeriesBoxComponent team={rightTeam} seriesNumber={5} />,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(container.querySelector(".right_series_box")).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { StatItem } from "../src/StatItem";
|
|
5
|
+
|
|
6
|
+
describe("StatItem", () => {
|
|
7
|
+
it("renders label and value", () => {
|
|
8
|
+
render(<StatItem id="goals" label="Goals" value={5} />);
|
|
9
|
+
|
|
10
|
+
expect(screen.getByText("Goals")).toBeInTheDocument();
|
|
11
|
+
expect(screen.getByText("5")).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("applies correct CSS class based on id", () => {
|
|
15
|
+
const { container } = render(
|
|
16
|
+
<StatItem id="assists" label="Assists" value={3} />,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(
|
|
20
|
+
container.querySelector(".stat_box_statistic_player_assists"),
|
|
21
|
+
).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders string values", () => {
|
|
25
|
+
render(<StatItem id="name" label="Name" value="TestPlayer" />);
|
|
26
|
+
expect(screen.getByText("TestPlayer")).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders numeric zero", () => {
|
|
30
|
+
render(<StatItem id="saves" label="Saves" value={0} />);
|
|
31
|
+
expect(screen.getByText("0")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
// Mock the rl-socket-hook module
|
|
5
|
+
export const useEventSelector = vi.fn(() => null);
|
|
6
|
+
export const useEvent = vi.fn(() => null);
|
|
7
|
+
export const RLProvider = ({ children }: { children: React.ReactNode }) =>
|
|
8
|
+
React.createElement("div", { "data-testid": "rl-provider" }, children);
|
|
9
|
+
export const WebsocketData = () =>
|
|
10
|
+
React.createElement("div", { "data-testid": "websocket-data" });
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Broadcast, OverlayObject } from "../src/types";
|
|
2
|
+
|
|
3
|
+
export const mockBroadcast: Broadcast = {
|
|
4
|
+
id: "broadcast-1",
|
|
5
|
+
name: "Test Broadcast",
|
|
6
|
+
status: "active",
|
|
7
|
+
overlay_id: "overlay-1",
|
|
8
|
+
featured: false,
|
|
9
|
+
top_info_text: "Grand Finals",
|
|
10
|
+
series_number: 5,
|
|
11
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
12
|
+
created_by: null,
|
|
13
|
+
teams: [
|
|
14
|
+
{
|
|
15
|
+
id: "0",
|
|
16
|
+
broadcast_id: "broadcast-1",
|
|
17
|
+
session_id: null,
|
|
18
|
+
organization_id: null,
|
|
19
|
+
color_id: null,
|
|
20
|
+
name: "Blue Strikers",
|
|
21
|
+
series_score: 2,
|
|
22
|
+
logo: null,
|
|
23
|
+
side: "blue",
|
|
24
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
25
|
+
created_by: null,
|
|
26
|
+
color: {
|
|
27
|
+
id: "color-1",
|
|
28
|
+
name: "Blue",
|
|
29
|
+
primary_color: "#0052cc",
|
|
30
|
+
secondary_color: "#ffffff",
|
|
31
|
+
mutual_color: "#00a8ff",
|
|
32
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
33
|
+
created_by: null,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "1",
|
|
38
|
+
broadcast_id: "broadcast-1",
|
|
39
|
+
session_id: null,
|
|
40
|
+
organization_id: null,
|
|
41
|
+
color_id: null,
|
|
42
|
+
name: "Orange Thunder",
|
|
43
|
+
series_score: 1,
|
|
44
|
+
logo: null,
|
|
45
|
+
side: "orange",
|
|
46
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
47
|
+
created_by: null,
|
|
48
|
+
color: {
|
|
49
|
+
id: "color-2",
|
|
50
|
+
name: "Orange",
|
|
51
|
+
primary_color: "#ff6600",
|
|
52
|
+
secondary_color: "#ffffff",
|
|
53
|
+
mutual_color: "#ffaa00",
|
|
54
|
+
created_at: "2025-01-01T00:00:00Z",
|
|
55
|
+
created_by: null,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const mockOverlay: OverlayObject = {
|
|
62
|
+
components: [
|
|
63
|
+
{
|
|
64
|
+
id: "comp-1",
|
|
65
|
+
name: "Scoreboard",
|
|
66
|
+
css: ".scoreboard_box { display: flex; }",
|
|
67
|
+
code_id: "Scoreboard",
|
|
68
|
+
position: { top: 0, left: 400, width: 500, height: 100 },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "comp-2",
|
|
72
|
+
name: "Teams",
|
|
73
|
+
css: ".team_box { display: flex; }",
|
|
74
|
+
code_id: "Teams",
|
|
75
|
+
position: { top: 200, left: 0, width: 300, height: 600 },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const mockPlayerState = {
|
|
81
|
+
id: "player-1",
|
|
82
|
+
name: "TestPlayer",
|
|
83
|
+
team: 0,
|
|
84
|
+
score: 200,
|
|
85
|
+
goals: 1,
|
|
86
|
+
assists: 2,
|
|
87
|
+
shots: 5,
|
|
88
|
+
saves: 3,
|
|
89
|
+
touches: 40,
|
|
90
|
+
boost: 75,
|
|
91
|
+
speed: 1200,
|
|
92
|
+
isDead: false,
|
|
93
|
+
isSonic: false,
|
|
94
|
+
hasBall: false,
|
|
95
|
+
location: { X: 100.5, Y: -200.3, Z: 50.0 },
|
|
96
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { componentRegistry } from "../src/registry";
|
|
3
|
+
|
|
4
|
+
describe("componentRegistry", () => {
|
|
5
|
+
it("contains all expected components", () => {
|
|
6
|
+
const expectedKeys = [
|
|
7
|
+
"Scoreboard",
|
|
8
|
+
"Teams",
|
|
9
|
+
"TargetPlayer",
|
|
10
|
+
"TargetBoost",
|
|
11
|
+
"Replay",
|
|
12
|
+
"Timer",
|
|
13
|
+
"PlayerBoost",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const key of expectedKeys) {
|
|
17
|
+
expect(componentRegistry).toHaveProperty(key);
|
|
18
|
+
// Components may be functions or memo objects
|
|
19
|
+
expect(componentRegistry[key]).toBeTruthy();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("does not contain unexpected entries", () => {
|
|
24
|
+
const keys = Object.keys(componentRegistry);
|
|
25
|
+
expect(keys.length).toBe(7);
|
|
26
|
+
});
|
|
27
|
+
});
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|