Files
OpenFrontIO/tests/InputHandler.test.ts
Ivan Batsulin e5e1211480 feat: add Shift+ modifier support for keybinds (#3679)
## Description:

This PR adds support for `Shift+<key>` keybind combinations across the
entire keybind system.

Previously, keybinds only supported a single key (e.g. `KeyB` for boat
attack). Now any keybind can be configured as `Shift+KeyB`, which will
only trigger when Shift is held down simultaneously.

Enables to use Shift + A for "select all" feature from #3677 

**Changes:**
- `InputHandler.ts`: Added `parseKeybind()` helper that parses
`"Shift+KeyB"` → `{ shift: true, code: "KeyB" }`. Added
`keybindMatchesEvent()` for consistent matching across all keyup/keydown
handlers. Updated `resolveBuildKeybind()` and all keybind comparisons to
respect the shift modifier.
- `SettingKeybind.ts`: When recording a keybind, lone modifier keys
(Shift, Ctrl, etc.) are skipped — the component waits for the actual
key. If Shift is held when the key is pressed, the value is stored as
`"Shift+<code>"`.
- `Utils.ts`: `formatKeyForDisplay()` now handles the `Shift+` prefix,
displaying e.g. `"Shift+B"`.
- `tests/InputHandler.test.ts`: Added 6 tests covering Shift+ keybind
matching, negative cases (plain key not triggering Shift-bound action),
coexistence of `Digit1` and `Shift+Digit1` on different actions, and
Numpad alias support with Shift.

## 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

## UI changes:

<img width="2255" height="2070" alt="CleanShot 2026-04-15 at 20 23
25@2x"
src="https://github.com/user-attachments/assets/96c19fc3-6294-40b7-82eb-3fde52b71618"
/>


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

fghjk_60845
2026-04-16 19:46:01 -07:00

863 lines
23 KiB
TypeScript

import {
AutoUpgradeEvent,
ConfirmGhostStructureEvent,
InputHandler,
} from "../src/client/InputHandler";
import { UIState } from "../src/client/graphics/UIState";
import { EventBus } from "../src/core/EventBus";
import { UnitType } from "../src/core/game/Game";
import { GameView } from "../src/core/game/GameView";
import { KEYBINDS_KEY, UserSettings } from "../src/core/game/UserSettings";
class MockPointerEvent {
button: number;
clientX: number;
clientY: number;
x: number;
y: number;
pointerId: number;
type: string;
pointerType: string;
preventDefault: () => void;
constructor(type: string, init: any) {
this.type = type;
this.button = init.button;
this.clientX = init.clientX;
this.clientY = init.clientY;
this.x = init.x ?? init.clientX;
this.y = init.y ?? init.clientY;
this.pointerId = init.pointerId;
this.pointerType = init.pointerType ?? "mouse";
this.preventDefault = vi.fn();
}
}
global.PointerEvent = MockPointerEvent as any;
describe("InputHandler AutoUpgrade", () => {
let inputHandler: InputHandler;
let mockGameView: GameView;
let eventBus: EventBus;
let mockCanvas: HTMLCanvasElement;
let testSettings: UserSettings;
beforeEach(() => {
testSettings = new UserSettings();
testSettings.removeCached(KEYBINDS_KEY, false);
mockGameView = { inSpawnPhase: () => false } as GameView;
mockCanvas = document.createElement("canvas");
mockCanvas.width = 800;
mockCanvas.height = 600;
eventBus = new EventBus();
inputHandler = new InputHandler(
mockGameView,
{
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
},
mockCanvas,
eventBus,
);
});
afterEach(() => {
inputHandler.destroy();
});
describe("Middle Mouse Button Handling", () => {
test("should emit AutoUpgradeEvent on middle mouse button press", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
});
test("should emit MouseDownEvent on left mouse button press instead of AutoUpgradeEvent", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 0,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
const calls = mockEmit.mock.calls;
const lastCall = calls[calls.length - 1];
expect(lastCall[0]).not.toBeInstanceOf(AutoUpgradeEvent);
});
test("should not emit AutoUpgradeEvent on right mouse button press", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 2,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).not.toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
});
test("should handle multiple middle mouse button presses", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
clientX: 100,
clientY: 200,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent1);
const pointerEvent2 = new PointerEvent("pointerdown", {
button: 1,
clientX: 300,
clientY: 400,
pointerId: 2,
});
inputHandler["onPointerDown"](pointerEvent2);
expect(mockEmit).toHaveBeenCalledTimes(2);
expect(mockEmit).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
x: 100,
y: 200,
}),
);
expect(mockEmit).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
x: 300,
y: 400,
}),
);
});
test("should handle middle mouse button press with zero coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 0,
clientY: 0,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 0,
y: 0,
}),
);
});
test("should handle middle mouse button press with negative coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: -100,
clientY: -200,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: -100,
y: -200,
}),
);
});
test("should handle middle mouse button press with decimal coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 100.5,
clientY: 200.7,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 100.5,
y: 200.7,
}),
);
});
});
describe("Spawn Phase Handling", () => {
test("should emit MouseUpEvent and not ContextMenuEvent on left click release during spawn phase", () => {
mockGameView.inSpawnPhase = () => true;
const mockEmit = vi.spyOn(eventBus, "emit");
inputHandler["userSettings"].leftClickOpensMenu = () => true;
const pointerEvent = new PointerEvent("pointerup", {
button: 0,
clientX: 150,
clientY: 250,
});
inputHandler["lastPointerDownX"] = 149;
inputHandler["lastPointerDownY"] = 249;
inputHandler["onPointerUp"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("MouseUpEvent");
expect(emittedTypes).not.toContain("ContextMenuEvent");
});
test("should suppress/ignore context menu events during spawn phase", () => {
mockGameView.inSpawnPhase = () => true;
const mockEmit = vi.spyOn(eventBus, "emit");
const mouseEvent = new MouseEvent("contextmenu", {
clientX: 150,
clientY: 250,
});
const preventDefaultSpy = vi.spyOn(mouseEvent, "preventDefault");
inputHandler["onContextMenu"](mouseEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).not.toContain("ContextMenuEvent");
});
});
describe("Pointer Event Handling", () => {
test("should handle pointer events with different pointer IDs", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
clientX: 100,
clientY: 200,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent1);
const pointerEvent2 = new PointerEvent("pointerdown", {
button: 1,
clientX: 300,
clientY: 400,
pointerId: 2,
});
inputHandler["onPointerDown"](pointerEvent2);
expect(mockEmit).toHaveBeenCalledTimes(2);
});
test("should handle pointer events with same pointer ID", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent1 = new PointerEvent("pointerdown", {
button: 1,
clientX: 100,
clientY: 200,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent1);
const pointerEvent2 = new PointerEvent("pointerdown", {
button: 1,
clientX: 300,
clientY: 400,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent2);
expect(mockEmit).toHaveBeenCalledTimes(2);
});
});
describe("Edge Cases", () => {
test("should handle very large coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: Number.MAX_SAFE_INTEGER,
clientY: Number.MAX_SAFE_INTEGER,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: Number.MAX_SAFE_INTEGER,
y: Number.MAX_SAFE_INTEGER,
}),
);
});
test("should handle very small coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: Number.MIN_SAFE_INTEGER,
clientY: Number.MIN_SAFE_INTEGER,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: Number.MIN_SAFE_INTEGER,
y: Number.MIN_SAFE_INTEGER,
}),
);
});
test("should handle NaN coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: NaN,
clientY: NaN,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: NaN,
y: NaN,
}),
);
});
test("should handle Infinity coordinates", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: Infinity,
clientY: -Infinity,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockEmit).toHaveBeenCalledWith(
expect.objectContaining({
x: Infinity,
y: -Infinity,
}),
);
});
});
describe("Integration with Event Bus", () => {
test("should allow event listeners to receive AutoUpgradeEvents", () => {
const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener);
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockListener).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
});
test("should allow multiple listeners for AutoUpgradeEvent", () => {
const mockListener1 = vi.fn();
const mockListener2 = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener1);
eventBus.on(AutoUpgradeEvent, mockListener2);
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockListener1).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
expect(mockListener2).toHaveBeenCalledWith(
expect.objectContaining({
x: 150,
y: 250,
}),
);
});
test("should not call unsubscribed listeners", () => {
const mockListener = vi.fn();
eventBus.on(AutoUpgradeEvent, mockListener);
eventBus.off(AutoUpgradeEvent, mockListener);
const pointerEvent = new PointerEvent("pointerdown", {
button: 1,
clientX: 150,
clientY: 250,
pointerId: 1,
});
inputHandler["onPointerDown"](pointerEvent);
expect(mockListener).not.toHaveBeenCalled();
});
});
describe("Keybinds JSON parsing", () => {
test("parses nested object values and flattens them to strings", () => {
const nested = {
moveUp: { key: "moveUp", value: "KeyZ" },
};
testSettings.setKeybinds(nested);
inputHandler.initialize();
expect((inputHandler as any).keybinds.moveUp).toBe("KeyZ");
});
test("accepts legacy string values", () => {
testSettings.setKeybinds({ moveUp: "KeyX" });
inputHandler.initialize();
expect((inputHandler as any).keybinds.moveUp).toBe("KeyX");
});
test("ignores non-string values and preserves defaults, removes 'Null' for unbound keys", () => {
const mixed = {
moveUp: { key: "moveUp", value: null },
moveLeft: "Null",
};
testSettings.setKeybinds(mixed);
inputHandler.initialize();
expect((inputHandler as any).keybinds.moveUp).toBe("KeyW");
// "Null" entries are removed entirely to indicate unbound keybind
expect((inputHandler as any).keybinds.moveLeft).toBeUndefined();
});
test("handles invalid JSON gracefully and warns", () => {
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
testSettings.setKeybinds("not a json");
inputHandler.initialize();
expect(spy).toHaveBeenCalled();
// default remains when parsing fails
expect((inputHandler as any).keybinds.moveUp).toBe("KeyW");
spy.mockRestore();
});
});
describe("Enter key confirm ghost structure", () => {
let uiState: UIState;
beforeEach(() => {
uiState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
test("emits ConfirmGhostStructureEvent on Enter when ghost structure is set", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
uiState.ghostStructure = UnitType.City;
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
expect(mockEmit).toHaveBeenCalledWith(
expect.any(ConfirmGhostStructureEvent),
);
});
test("emits ConfirmGhostStructureEvent on NumpadEnter when ghost structure is set", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
uiState.ghostStructure = UnitType.Factory;
window.dispatchEvent(
new KeyboardEvent("keydown", { code: "NumpadEnter" }),
);
expect(mockEmit).toHaveBeenCalledWith(
expect.any(ConfirmGhostStructureEvent),
);
});
test("does not emit ConfirmGhostStructureEvent on Enter when no ghost structure", () => {
const mockEmit = vi.spyOn(eventBus, "emit");
expect(uiState.ghostStructure).toBeNull();
window.dispatchEvent(new KeyboardEvent("keydown", { code: "Enter" }));
const confirmCalls = mockEmit.mock.calls.filter(
(call) => call[0] instanceof ConfirmGhostStructureEvent,
);
expect(confirmCalls).toHaveLength(0);
});
});
describe("Numpad number keys for build keybinds", () => {
beforeEach(() => {
inputHandler.destroy();
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
test("Numpad1 sets ghost structure to City when buildCity is Digit1", () => {
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
});
test("Numpad5 sets ghost structure to MissileSilo when buildMissileSilo is Digit5", () => {
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad5", key: "5" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MissileSilo);
});
test("Numpad0 sets ghost structure to MIRV when buildMIRV is Digit0", () => {
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad0", key: "0" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.MIRV);
});
});
describe("Build keybind two-phase matching (exact code first, then digit/Numpad alias)", () => {
beforeEach(() => {
inputHandler.destroy();
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
});
test("exact code match wins: Digit1 sets City when buildCity=Digit1 and buildFactory=Numpad1", () => {
testSettings.setKeybinds({
buildCity: "Digit1",
buildFactory: "Numpad1",
});
inputHandler.destroy();
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Digit1", key: "1" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
});
test("exact code match wins: Numpad1 sets Factory when buildCity=Digit1 and buildFactory=Numpad1", () => {
testSettings.setKeybinds({
buildCity: "Digit1",
buildFactory: "Numpad1",
});
inputHandler.destroy();
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.Factory);
});
test("digit alias used when no exact match: Numpad1 sets City when only buildCity=Digit1", () => {
testSettings.setKeybinds({ buildCity: "Digit1" });
inputHandler.destroy();
const uiState: UIState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad1", key: "1" }),
);
expect(inputHandler["uiState"].ghostStructure).toBe(UnitType.City);
});
});
describe("Shift+ keybind support", () => {
let uiState: UIState;
beforeEach(() => {
inputHandler.destroy();
uiState = {
attackRatio: 20,
ghostStructure: null,
rocketDirectionUp: true,
overlappingRailroads: [],
ghostRailPaths: [],
} as UIState;
});
test("Shift+Digit1 sets City when buildCity is bound to Shift+Digit1", () => {
testSettings.setKeybinds({ buildCity: "Shift+Digit1" });
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Digit1", shiftKey: true }),
);
expect(uiState.ghostStructure).toBe(UnitType.City);
});
test("plain Digit1 does NOT trigger buildCity when bound to Shift+Digit1", () => {
testSettings.setKeybinds({ buildCity: "Shift+Digit1" });
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Digit1", shiftKey: false }),
);
expect(uiState.ghostStructure).toBeNull();
});
test("Shift+KeyB triggers boatAttack when bound to Shift+KeyB", () => {
testSettings.setKeybinds({ boatAttack: "Shift+KeyB" });
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
const mockEmit = vi.spyOn(eventBus, "emit");
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "KeyB", shiftKey: true }),
);
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).toContain("DoBoatAttackEvent");
});
test("plain KeyB does NOT trigger boatAttack when bound to Shift+KeyB", () => {
testSettings.setKeybinds({ boatAttack: "Shift+KeyB" });
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
const mockEmit = vi.spyOn(eventBus, "emit");
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "KeyB", shiftKey: false }),
);
const emittedTypes = mockEmit.mock.calls.map(
(call) => call[0].constructor.name,
);
expect(emittedTypes).not.toContain("DoBoatAttackEvent");
});
test("Shift+Digit1 and Digit1 can be bound to different actions without conflict", () => {
testSettings.setKeybinds({
buildCity: "Digit1",
buildFactory: "Shift+Digit1",
});
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Digit1", shiftKey: false }),
);
expect(uiState.ghostStructure).toBe(UnitType.City);
uiState.ghostStructure = null;
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Digit1", shiftKey: true }),
);
expect(uiState.ghostStructure).toBe(UnitType.Factory);
});
test("Numpad alias works with Shift+Digit keybind", () => {
testSettings.setKeybinds({ buildCity: "Shift+Digit1" });
inputHandler = new InputHandler(
mockGameView,
uiState,
mockCanvas,
eventBus,
);
inputHandler.initialize();
window.dispatchEvent(
new KeyboardEvent("keyup", { code: "Numpad1", shiftKey: true }),
);
expect(uiState.ghostStructure).toBe(UnitType.City);
});
});
});