first commmit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
DEFAULT_LAYOUT_DELTA,
|
||||
LAYOUT_DELTA_BOUNDS,
|
||||
LayoutCustomizer,
|
||||
applyLayoutDelta,
|
||||
sanitiseLayoutDelta,
|
||||
} from '@ui/LayoutCustomizer';
|
||||
import { DEFAULT_LAYOUT } from '@ui/InputModel';
|
||||
import { StorageMgr } from '@common/StorageMgr';
|
||||
import { STORAGE_KEY } from '@common/Constants';
|
||||
|
||||
function mem() {
|
||||
const m = new Map<string, string>();
|
||||
return {
|
||||
getItem: (k: string) => (m.has(k) ? (m.get(k) as string) : null),
|
||||
setItem: (k: string, v: string) => {
|
||||
m.set(k, v);
|
||||
},
|
||||
removeItem: (k: string) => {
|
||||
m.delete(k);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('sanitiseLayoutDelta', () => {
|
||||
it('returns a defensive copy of the default when given null', () => {
|
||||
const d = sanitiseLayoutDelta(null);
|
||||
expect(d).toEqual(DEFAULT_LAYOUT_DELTA);
|
||||
d.opacity = 0.1;
|
||||
expect(DEFAULT_LAYOUT_DELTA.opacity).toBe(0.7); // ensure we did not mutate
|
||||
});
|
||||
|
||||
it('clamps offset beyond the allowed range', () => {
|
||||
const d = sanitiseLayoutDelta({ joystickOffset: { dx: 9999, dy: -9999 } });
|
||||
expect(d.joystickOffset.dx).toBe(LAYOUT_DELTA_BOUNDS.offsetPxMax);
|
||||
expect(d.joystickOffset.dy).toBe(-LAYOUT_DELTA_BOUNDS.offsetPxMax);
|
||||
});
|
||||
|
||||
it('clamps size scale and opacity to allowed ranges', () => {
|
||||
const d = sanitiseLayoutDelta({ buttonSizeScale: 5, opacity: 2 });
|
||||
expect(d.buttonSizeScale).toBe(LAYOUT_DELTA_BOUNDS.sizeScaleMax);
|
||||
expect(d.opacity).toBe(LAYOUT_DELTA_BOUNDS.opacityMax);
|
||||
});
|
||||
|
||||
it('replaces NaN-like inputs with safe midpoints', () => {
|
||||
const d = sanitiseLayoutDelta({ buttonSizeScale: NaN });
|
||||
expect(d.buttonSizeScale).toBeGreaterThanOrEqual(LAYOUT_DELTA_BOUNDS.sizeScaleMin);
|
||||
expect(d.buttonSizeScale).toBeLessThanOrEqual(LAYOUT_DELTA_BOUNDS.sizeScaleMax);
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyLayoutDelta', () => {
|
||||
it('produces a layout identical to baseline when delta is default', () => {
|
||||
const result = applyLayoutDelta(DEFAULT_LAYOUT, DEFAULT_LAYOUT_DELTA);
|
||||
expect(result.joystick.cx).toBe(DEFAULT_LAYOUT.joystick.cx);
|
||||
expect(result.opacity).toBe(DEFAULT_LAYOUT_DELTA.opacity);
|
||||
});
|
||||
|
||||
it('shifts centres and scales widths', () => {
|
||||
const delta = sanitiseLayoutDelta({
|
||||
joystickOffset: { dx: 30, dy: 10 },
|
||||
buttonSizeScale: 1.2,
|
||||
opacity: 0.9,
|
||||
});
|
||||
const r = applyLayoutDelta(DEFAULT_LAYOUT, delta);
|
||||
expect(r.joystick.cx).toBe(DEFAULT_LAYOUT.joystick.cx + 30);
|
||||
expect(r.joystick.cy).toBe(DEFAULT_LAYOUT.joystick.cy + 10);
|
||||
expect(r.shuriken.w).toBeCloseTo(DEFAULT_LAYOUT.shuriken.w * 1.2);
|
||||
expect(r.opacity).toBe(0.9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LayoutCustomizer — persistence', () => {
|
||||
it('returns the default layout when nothing is stored (req 17.6)', () => {
|
||||
const cust = new LayoutCustomizer(DEFAULT_LAYOUT, new StorageMgr(mem()));
|
||||
const { delta, layout } = cust.loadLayout();
|
||||
expect(delta).toEqual(DEFAULT_LAYOUT_DELTA);
|
||||
expect(layout.joystick.cx).toBe(DEFAULT_LAYOUT.joystick.cx);
|
||||
});
|
||||
|
||||
it('round-trips a custom delta through saveDelta() / loadLayout()', () => {
|
||||
const storage = new StorageMgr(mem());
|
||||
const cust = new LayoutCustomizer(DEFAULT_LAYOUT, storage);
|
||||
cust.saveDelta({
|
||||
joystickOffset: { dx: 20, dy: -15 },
|
||||
jumpOffset: { dx: 0, dy: 0 },
|
||||
shurikenOffset: { dx: -10, dy: 5 },
|
||||
ninjaSwordOffset: { dx: 0, dy: 0 },
|
||||
buttonSizeScale: 1.1,
|
||||
opacity: 0.85,
|
||||
});
|
||||
const { delta } = cust.loadLayout();
|
||||
expect(delta.joystickOffset.dx).toBe(20);
|
||||
expect(delta.shurikenOffset.dx).toBe(-10);
|
||||
expect(delta.opacity).toBe(0.85);
|
||||
});
|
||||
|
||||
it('reset() clears the stored layout', () => {
|
||||
const storage = new StorageMgr(mem());
|
||||
const cust = new LayoutCustomizer(DEFAULT_LAYOUT, storage);
|
||||
cust.saveDelta({ ...DEFAULT_LAYOUT_DELTA, opacity: 1.0 });
|
||||
cust.reset();
|
||||
const { delta } = cust.loadLayout();
|
||||
expect(delta.opacity).toBe(DEFAULT_LAYOUT_DELTA.opacity);
|
||||
});
|
||||
|
||||
it('falls back to defaults when storage returns corrupted JSON (req 17.6)', () => {
|
||||
const driver = {
|
||||
getItem: () => 'not valid json',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
};
|
||||
const storage = new StorageMgr(driver);
|
||||
const cust = new LayoutCustomizer(DEFAULT_LAYOUT, storage);
|
||||
const { delta } = cust.loadLayout();
|
||||
expect(delta).toEqual(DEFAULT_LAYOUT_DELTA);
|
||||
});
|
||||
|
||||
it('uses the kl_control_layout storage key (req 17.2)', () => {
|
||||
const driver = mem();
|
||||
const setSpy = jest.fn(driver.setItem);
|
||||
driver.setItem = setSpy;
|
||||
const storage = new StorageMgr(driver);
|
||||
const cust = new LayoutCustomizer(DEFAULT_LAYOUT, storage);
|
||||
cust.saveDelta(DEFAULT_LAYOUT_DELTA);
|
||||
expect(setSpy).toHaveBeenCalled();
|
||||
expect(setSpy.mock.calls[0][0]).toBe(STORAGE_KEY.ControlLayout);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user