Files
OpenFrontIO/tests/client/components/FluentSlider.test.ts
T
Ryan 5e6c90d9bb Main Menu UI Overhaul (#2829)
## Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see
everything.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
2026-01-09 20:26:34 -08:00

296 lines
9.4 KiB
TypeScript

import { FluentSlider } from "../../../src/client/components/FluentSlider";
// Mock the translateText function
vi.mock("../../../src/client/Utils", () => ({
translateText: vi.fn((key: string) => key),
}));
describe("FluentSlider", () => {
let slider: FluentSlider;
beforeEach(async () => {
// Define the custom element if not already defined
if (!customElements.get("fluent-slider")) {
customElements.define("fluent-slider", FluentSlider);
}
slider = document.createElement("fluent-slider") as FluentSlider;
document.body.appendChild(slider);
await slider.updateComplete;
});
afterEach(() => {
document.body.removeChild(slider);
});
describe("Initialization", () => {
it("should initialize with default values", () => {
expect(slider.value).toBe(0);
expect(slider.min).toBe(0);
expect(slider.max).toBe(400);
expect(slider.step).toBe(1);
expect(slider.labelKey).toBe("");
expect(slider.disabledKey).toBe("");
});
it("should accept custom property values", async () => {
slider.value = 150;
slider.min = 10;
slider.max = 300;
slider.step = 5;
slider.labelKey = "host_modal.bots";
slider.disabledKey = "host_modal.bots_disabled";
await slider.updateComplete;
expect(slider.value).toBe(150);
expect(slider.min).toBe(10);
expect(slider.max).toBe(300);
expect(slider.step).toBe(5);
expect(slider.labelKey).toBe("host_modal.bots");
expect(slider.disabledKey).toBe("host_modal.bots_disabled");
});
});
describe("Value Updates from Range Slider", () => {
it("should update value when slider input changes", async () => {
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
expect(rangeInput).toBeTruthy();
// Simulate slider input
rangeInput.valueAsNumber = 250;
rangeInput.dispatchEvent(new Event("input", { bubbles: true }));
await slider.updateComplete;
expect(slider.value).toBe(250);
});
it("should update value when slider change event fires", async () => {
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
rangeInput.valueAsNumber = 100;
rangeInput.dispatchEvent(new Event("change", { bubbles: true }));
await slider.updateComplete;
expect(slider.value).toBe(100);
});
});
describe("Value-Changed Event - CRITICAL FOR BUG FIX", () => {
it("should dispatch CustomEvent with detail.value (not event.target.value)", async () => {
const eventSpy = vi.fn();
slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
rangeInput.valueAsNumber = 200;
rangeInput.dispatchEvent(new Event("change", { bubbles: true }));
await slider.updateComplete;
expect(eventSpy).toHaveBeenCalled();
const event = eventSpy.mock.calls[0][0] as CustomEvent<{
value: number;
}>;
// CRITICAL: Event must have detail.value, not target.value
expect(event.detail).toBeDefined();
expect(event.detail.value).toBe(200);
expect(event.bubbles).toBe(true);
expect(event.composed).toBe(true);
});
it("should not dispatch event on input, only on change", async () => {
const eventSpy = vi.fn();
slider.addEventListener("value-changed", eventSpy);
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
// Input event should NOT trigger value-changed
rangeInput.valueAsNumber = 150;
rangeInput.dispatchEvent(new Event("input", { bubbles: true }));
await slider.updateComplete;
expect(eventSpy).not.toHaveBeenCalled();
// Change event SHOULD trigger value-changed
rangeInput.dispatchEvent(new Event("change", { bubbles: true }));
await slider.updateComplete;
expect(eventSpy).toHaveBeenCalledTimes(1);
});
it("should work with the handler pattern used in HostLobbyModal", async () => {
// This simulates the actual handler code in HostLobbyModal.ts:656-660
const mockHandler = vi.fn((e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
// If we get here, the event structure is correct!
expect(value).toBeDefined();
expect(typeof value).toBe("number");
});
slider.addEventListener("value-changed", mockHandler);
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
rangeInput.valueAsNumber = 250;
rangeInput.dispatchEvent(new Event("change", { bubbles: true }));
await slider.updateComplete;
expect(mockHandler).toHaveBeenCalledTimes(1);
expect(slider.value).toBe(250);
});
it("should work with the handler pattern used in SinglePlayerModal", async () => {
// This simulates the actual handler code in SinglePlayerModal.ts:444-451
const mockHandler = vi.fn((e: Event) => {
const customEvent = e as CustomEvent<{ value: number }>;
const value = customEvent.detail.value;
if (isNaN(value) || value < 0 || value > 400) {
return;
}
expect(value).toBeGreaterThanOrEqual(0);
expect(value).toBeLessThanOrEqual(400);
});
slider.addEventListener("value-changed", mockHandler);
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
rangeInput.valueAsNumber = 350;
rangeInput.dispatchEvent(new Event("change", { bubbles: true }));
await slider.updateComplete;
expect(mockHandler).toHaveBeenCalledTimes(1);
expect(slider.value).toBe(350);
});
});
describe("Value Validation via Number Input", () => {
it("should clamp values to min", () => {
slider.min = 10;
slider.max = 100;
// Simulate the handleNumberChange logic
let testValue = 5; // Below min
if (isNaN(testValue)) {
testValue = slider.min;
}
if (testValue < slider.min) testValue = slider.min;
if (testValue > slider.max) testValue = slider.max;
expect(testValue).toBe(10);
});
it("should clamp values to max", () => {
slider.min = 10;
slider.max = 100;
let testValue = 150; // Above max
if (isNaN(testValue)) {
testValue = slider.min;
}
if (testValue < slider.min) testValue = slider.min;
if (testValue > slider.max) testValue = slider.max;
expect(testValue).toBe(100);
});
it("should default to min when NaN", () => {
slider.min = 5;
let testValue = NaN;
if (isNaN(testValue)) {
testValue = slider.min;
}
if (testValue < slider.min) testValue = slider.min;
if (testValue > slider.max) testValue = slider.max;
expect(testValue).toBe(5);
});
});
describe("Component Structure", () => {
it("should render a range input", () => {
const rangeInput = slider.querySelector('input[type="range"]');
expect(rangeInput).toBeTruthy();
});
it("should have correct range input properties", () => {
slider.value = 150;
slider.min = 0;
slider.max = 400;
slider.step = 1;
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
expect(rangeInput.min).toBe("0");
expect(rangeInput.max).toBe("400");
expect(rangeInput.step).toBe("1");
});
it("should render a span for the value display with role button", () => {
const valueSpan = slider.querySelector('span[role="button"]');
expect(valueSpan).toBeTruthy();
expect(valueSpan?.getAttribute("role")).toBe("button");
expect(valueSpan?.getAttribute("tabindex")).toBe("0");
});
});
describe("Bot Count Scenario - Regression Test", () => {
it("should correctly update bot count from 0 to 400 and dispatch proper events", async () => {
const capturedValues: number[] = [];
slider.addEventListener("value-changed", (e) => {
const customEvent = e as CustomEvent<{ value: number }>;
capturedValues.push(customEvent.detail.value);
});
slider.value = 0;
await slider.updateComplete;
const rangeInput = slider.querySelector(
'input[type="range"]',
) as HTMLInputElement;
// Simulate dragging the slider from 0 to 400
for (const val of [0, 100, 200, 300, 400]) {
rangeInput.valueAsNumber = val;
rangeInput.dispatchEvent(new Event("input", { bubbles: true })); // Updates display
rangeInput.dispatchEvent(new Event("change", { bubbles: true })); // Triggers event
await slider.updateComplete;
}
// Should have captured all change events (not input events)
expect(capturedValues).toEqual([0, 100, 200, 300, 400]);
expect(slider.value).toBe(400);
});
});
describe("Edge Cases", () => {
it("should handle min equal to max without NaN in style", async () => {
slider.min = 100;
slider.max = 100;
slider.value = 100;
await slider.updateComplete;
const rangeInput = slider.querySelector('input[type="range"]');
const style = rangeInput?.getAttribute("style");
expect(style).not.toContain("NaN");
expect(style).toContain("0%");
});
});
});