llm-semantic-visualizer / tests /LandscapeGate.test.js
hbauzan's picture
feat(ui): field-info tips and retire landscape gate
aaa664a
Raw
History Blame Contribute Delete
6.1 kB
import { describe, it, expect, beforeEach } from 'vitest';
import {
isPhonePortrait,
shouldShowLandscapeGate,
wasLandscapeGateDismissed,
dismissLandscapeGate,
LandscapeGate,
LANDSCAPE_DISMISS_KEY,
} from '../src/ui/LandscapeGate.js';
function createMockElement(tagName = 'div') {
const children = [];
const attrs = {};
const classList = new Set();
let _className = '';
let _textContent = '';
const element = {
tagName: tagName.toUpperCase(),
children,
parentNode: null,
style: {},
dataset: {},
classList: {
add: (...cls) => { cls.forEach((c) => c && classList.add(c)); _className = Array.from(classList).join(' '); },
remove: (...cls) => { cls.forEach((c) => classList.delete(c)); _className = Array.from(classList).join(' '); },
toggle: (cls, force) => {
if (force === true) classList.add(cls);
else if (force === false) classList.delete(cls);
else if (classList.has(cls)) classList.delete(cls);
else classList.add(cls);
_className = Array.from(classList).join(' ');
return classList.has(cls);
},
contains: (cls) => classList.has(cls),
},
appendChild: (child) => { children.push(child); child.parentNode = element; return child; },
removeChild: (child) => {
const i = children.indexOf(child);
if (i >= 0) children.splice(i, 1);
child.parentNode = null;
return child;
},
addEventListener: (type, fn) => {
if (!element._listeners) element._listeners = {};
if (!element._listeners[type]) element._listeners[type] = [];
element._listeners[type].push(fn);
},
removeEventListener: (type, fn) => {
if (!element._listeners?.[type]) return;
element._listeners[type] = element._listeners[type].filter((f) => f !== fn);
},
click: () => { (element._listeners?.click || []).forEach((fn) => fn({ type: 'click', target: element })); },
setAttribute: (k, v) => { attrs[k] = String(v); },
getAttribute: (k) => (attrs[k] !== undefined ? attrs[k] : null),
querySelector: (selector) => {
const cls = selector.startsWith('.') ? selector.slice(1) : null;
const id = selector.startsWith('#') ? selector.slice(1) : null;
for (const child of children) {
if (id && child.id === id) return child;
if (cls && child.classList.contains(cls)) return child;
const found = child.querySelector?.(selector);
if (found) return found;
}
return null;
},
set innerHTML(val) {
if (val === '') children.length = 0;
// Minimal: create dismiss button child for LandscapeGate markup
if (String(val).includes('landscape-gate-dismiss')) {
const btn = createMockElement('button');
btn.className = 'btn-primary landscape-gate-dismiss';
element.appendChild(btn);
const card = createMockElement('div');
card.className = 'landscape-gate-card';
element.appendChild(card);
}
},
set className(val) {
classList.clear();
_className = val || '';
String(val).split(/\s+/).forEach((c) => { if (c) classList.add(c); });
},
get className() { return _className; },
set textContent(val) { _textContent = val == null ? '' : String(val); },
get textContent() { return _textContent; },
id: '',
type: '',
};
return element;
}
function memorySession() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
};
}
const mockDoc = {
createElement: (tag) => {
const el = createMockElement(tag);
if (tag === 'button') el.type = 'button';
return el;
},
};
describe('isPhonePortrait', () => {
it('true only for phone-width portrait', () => {
expect(isPhonePortrait(() => ({ width: 390, height: 844 }))).toBe(true);
expect(isPhonePortrait(() => ({ width: 844, height: 390 }))).toBe(false);
expect(isPhonePortrait(() => ({ width: 1024, height: 1366 }))).toBe(false);
});
});
describe('shouldShowLandscapeGate', () => {
it('never shows — landscape cartel retired (portrait preferred)', () => {
expect(shouldShowLandscapeGate({ isMobile: true, isPortrait: true, dismissed: false })).toBe(false);
expect(shouldShowLandscapeGate({ isMobile: true, isPortrait: true, dismissed: true })).toBe(false);
expect(shouldShowLandscapeGate({ isMobile: true, isPortrait: false, dismissed: false })).toBe(false);
expect(shouldShowLandscapeGate({ isMobile: false, isPortrait: true, dismissed: false })).toBe(false);
});
});
describe('session dismiss', () => {
it('persists dismiss flag in sessionStorage', () => {
const session = memorySession();
expect(wasLandscapeGateDismissed(session)).toBe(false);
dismissLandscapeGate(session);
expect(session.getItem(LANDSCAPE_DISMISS_KEY)).toBe('1');
expect(wasLandscapeGateDismissed(session)).toBe(true);
});
});
describe('LandscapeGate', () => {
let parent;
let session;
beforeEach(() => {
parent = createMockElement('div');
session = memorySession();
});
it('never shows overlay even in phone portrait', () => {
const gate = new LandscapeGate({
parent,
doc: mockDoc,
sessionStorage: session,
isMobile: () => true,
isPortrait: () => true,
});
expect(gate.isVisible).toBe(false);
gate.refresh();
expect(gate.isVisible).toBe(false);
expect(gate.overlay.classList.contains('hidden')).toBe(true);
});
it('stays hidden on tablet/desktop and landscape phone', () => {
const desktop = new LandscapeGate({
parent,
doc: mockDoc,
sessionStorage: session,
isMobile: () => false,
isPortrait: () => true,
});
expect(desktop.isVisible).toBe(false);
const landscape = new LandscapeGate({
parent: createMockElement('div'),
doc: mockDoc,
sessionStorage: memorySession(),
isMobile: () => true,
isPortrait: () => false,
});
expect(landscape.isVisible).toBe(false);
});
});