Spaces:
Running
Running
File size: 6,103 Bytes
39508ae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | 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);
});
});
|