mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 23:21:58 +00:00
5e6c90d9bb
## 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
296 lines
9.4 KiB
TypeScript
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%");
|
|
});
|
|
});
|
|
});
|