WFDASH-WEBAPP/public/pos/test-longpress.html

305 lines
12 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>POS — long-press test harness</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%23ff385c'/></svg>">
<style>
:root{
--bg:#fff;--fg:#222;--muted:#ddd;--border:#ffe3e8;--accent:#ff385c;
--ok:#0f8a3f;--warn:#b14a00;--danger:#c92a2a;--surface:#fff7f8;
--font:'Inter',system-ui,-apple-system,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:var(--bg);color:var(--fg);font:14px/1.5 var(--font);-webkit-font-smoothing:antialiased}
body{padding:20px;max-width:640px;margin:0 auto}
h1{font-size:18px;margin-bottom:12px}
p{color:#666;margin-bottom:14px;font-size:13px}
button{font:inherit;padding:9px 14px;border:1px solid var(--muted);border-radius:8px;background:#fff;color:var(--fg);cursor:pointer;margin-right:8px;margin-bottom:8px}
button.primary{background:var(--accent);color:#fff;border-color:var(--accent)}
button:active{transform:translateY(1px)}
.status{margin-top:14px;padding:12px;border:1px solid var(--border);border-radius:8px;background:var(--surface);font-family:'SF Mono','Menlo','Consolas',monospace;font-size:12px;line-height:1.55;white-space:pre-wrap;min-height:60px}
.ok{color:var(--ok);font-weight:600}
.bad{color:var(--danger);font-weight:600}
.dim{color:#666}
.row{margin-top:14px;padding:12px;border:1px solid var(--border);border-radius:8px;background:#fff}
.row h3{font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:#666;margin-bottom:6px;font-weight:700}
.note{color:#b14a00;font-size:12px;margin-top:8px}
iframe{width:100%;height:1px;border:0;visibility:hidden}
</style>
</head>
<body>
<h1>POS long-press test harness</h1>
<p>Loads <code>pos-app.js</code> in a sandbox iframe, pre-populates a cart, and runs the long-press flow. The status box below reports each step.</p>
<button class="primary" id="bBoot" onclick="boot()">1. Boot the POS iframe</button>
<button id="bOpen" onclick="openCart()" disabled>2. Open cart drawer</button>
<button id="bPress" onclick="simulateLongPress()" disabled>3. Simulate long press (700ms)</button>
<button id="bBad" onclick="testInvalidSave()" disabled>4. Test save with empty qty (the bug fix)</button>
<button id="bCancel" onclick="testCancel()" disabled>5. Test Cancel closes the modal</button>
<div class="status" id="status">Ready. Tap "Boot" first.</div>
<div class="row">
<h3>How this works</h3>
<p>The iframe loads <code>pos-config.js</code>, <code>theme.js</code>, <code>letterhead-config.js</code>, and <code>pos-app.js</code> from this server. Before loading, we set <code>localStorage['pos.cart']</code> to a fake cart entry so the boot picks it up. The same-origin iframe can read the cart state via <code>window.__pos.openEditLine(id)</code>, which is what the long-press timer calls internally.</p>
<p>Step 3 dispatches a synthetic <code>pointerdown</code> on the cart line, waits 700ms (timer is 600ms), then checks if the modal opened.</p>
<p class="note">This is a temporary test page. Delete it after the bug fix is verified.</p>
</div>
<iframe id="pos" src="about:blank"></iframe>
<script>
// === pre-populate the cart for pos-app.js's loadCart() ===
const FAKE_CART = [
{
id: 'TEST-1',
qty: 2,
p: {
id: 'TEST-1',
sku: 'TEST-1',
description: 'Test Widget',
unit: 'ea',
cost_price: 30,
selling_price: 45,
},
},
];
localStorage.setItem('pos.cart', JSON.stringify(FAKE_CART));
// Also inject a fake active letterhead so the slip preview on
// settings.html can render without the API. Letterhead-config.js
// looks for these keys in localStorage.
localStorage.setItem('pos.letterhead.activeId', JSON.stringify('5wezvypn6fy47p9'));
localStorage.setItem('pos.letterhead.active', JSON.stringify({
id: '5wezvypn6fy47p9',
display_name: 'WESTSCOTT GROUP OF COMPANIES',
company: 'WESTSCOTT GROUP OF COMPANIES',
address: '47 Station Street, Carletonville, 2499',
phone: '+27 18 786 2897',
email: 'quotes@wfsales.co.za',
vat_number: '4590136331',
registration_number: '',
footer_text: 'Thank you for your business.',
logo: '',
logo_url: '',
is_active: true,
accent_color: '#FFBF00',
vat_percent: 15,
collection_id: 'pbc_1010367695',
}));
localStorage.setItem('pos.letterhead.list', JSON.stringify([
{ id: '5wezvypn6fy47p9', display_name: 'WESTSCOTT GROUP OF COMPANIES', company: 'WESTSCOTT GROUP OF COMPANIES', vat_percent: 15, accent_color: '#FFBF00', has_logo: false, is_active: true },
]));
// Pre-set an API key so pos-config.js skips the prompt() (which
// throws in some headless / test browser contexts and breaks every
// subsequent script that depends on window.posFetch being defined).
localStorage.setItem('pos.apiKey', 'test-key-bypasses-prompt');
// === SW + cache cleanup ===
// The fix we're verifying was committed after the most recent SW cache
// bump, but the in-browser SW can still be holding the older copy.
// The cleanup MUST complete before we boot the iframe, otherwise the
// iframe loads the cached pos-app.js (no fix). Boot stays disabled
// until cleanup finishes.
const bBoot = document.getElementById('bBoot');
const bOpen = document.getElementById('bOpen');
bBoot.disabled = true;
bBoot.textContent = '1. Boot the POS iframe (clearing cache…)';
(async function cleanup() {
if ('serviceWorker' in navigator) {
try {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map(r => r.unregister()));
} catch (e) { /* ignore */ }
}
if ('caches' in window) {
try {
const names = await caches.keys();
await Promise.all(names.map(n => caches.delete(n)));
} catch (e) { /* ignore */ }
}
bBoot.disabled = false;
bBoot.textContent = '1. Boot the POS iframe';
})();
const POS_URL = 'pos.html';
const statusEl = document.getElementById('status');
function setStatus(text, kind) {
statusEl.innerHTML = '';
const span = document.createElement('span');
span.className = kind || '';
span.textContent = text;
statusEl.appendChild(span);
}
function setStatusMulti(parts) {
statusEl.innerHTML = '';
parts.forEach(p => {
const span = document.createElement('span');
if (p.kind) span.className = p.kind;
if (p.dim) span.className = (span.className + ' dim').trim();
span.textContent = p.text + '\n';
statusEl.appendChild(span);
});
}
const iframe = document.getElementById('pos');
let booted = false;
function boot() {
if (booted) return;
// Cache-bust the iframe URL so the browser never serves a cached
// copy of pos.html either.
iframe.src = POS_URL + '?cb=' + Date.now();
iframe.addEventListener('load', () => {
booted = true;
bBoot.disabled = true;
bOpen.disabled = false;
setStatusMulti([
{ text: '✓ Iframe loaded with FRESH pos-app.js (cache-busted)', kind: 'ok' },
{ text: 'localStorage["pos.cart"] was set to a fake entry before load', dim: true },
{ text: 'Now tap "Open cart drawer" to render the cart with our test item.', dim: true },
]);
}, { once: true });
}
function openCart() {
if (!booted) return setStatus('Boot the iframe first.', 'bad');
try {
iframe.contentWindow.__pos.openDrawer();
// The drawer renders the cart from state.cart. Our fake item
// should be in the list.
setTimeout(() => {
const line = iframe.contentDocument.querySelector('.line[data-id="TEST-1"]');
if (line) {
setStatusMulti([
{ text: '✓ Cart drawer opened, line "TEST-1" rendered', kind: 'ok' },
{ text: 'Next: tap "Simulate long press" to trigger pointerdown.', dim: true },
]);
document.getElementById('bPress').disabled = false;
document.getElementById('bBad').disabled = false;
document.getElementById('bCancel').disabled = false;
} else {
setStatusMulti([
{ text: '✗ Cart drawer opened but no .line[data-id="TEST-1"] in the DOM', kind: 'bad' },
{ text: 'Possible causes: state.cart was empty (loadCart order), or the renderCartDrawer did not find the item.', dim: true },
]);
}
}, 150);
} catch (e) {
setStatus('openCart failed: ' + e.message, 'bad');
}
}
function simulateLongPress() {
if (!booted) return setStatus('Boot first.', 'bad');
const doc = iframe.contentDocument;
const line = doc.querySelector('.line[data-id="TEST-1"]');
if (!line) return setStatus('No cart line in DOM. Open cart first.', 'bad');
// Synthesize a pointerdown event. The real long-press uses
// pointerType:touch but pointerType:mouse works for the JS path.
const pd = new PointerEvent('pointerdown', {
bubbles: true,
cancelable: true,
pointerType: 'mouse',
pointerId: 1,
isPrimary: true,
});
line.dispatchEvent(pd);
setStatusMulti([
{ text: '→ Dispatched pointerdown on the cart line' },
{ text: 'Waiting 700ms for the 600ms timer to fire openEditLine...', dim: true },
]);
setTimeout(() => {
const modal = doc.getElementById('editSheet');
const backdrop = doc.getElementById('editBackdrop');
const open = modal && modal.classList.contains('show');
const backdropOpen = backdrop && backdrop.classList.contains('show');
if (open && backdropOpen) {
setStatusMulti([
{ text: '✓ Long press detected. Edit modal opened (backdrop + sheet both have .show).', kind: 'ok' },
{ text: 'Now tap "Test save with empty qty" to verify the original bug is fixed.', dim: true },
]);
} else {
setStatusMulti([
{ text: '✗ Long press did not open the edit modal', kind: 'bad' },
{ text: 'modal.show=' + open + ' backdrop.show=' + backdropOpen, dim: true },
]);
}
}, 700);
}
function testInvalidSave() {
if (!booted) return setStatus('Boot first.', 'bad');
const doc = iframe.contentDocument;
const modal = doc.getElementById('editSheet');
if (!modal || !modal.classList.contains('show')) {
return setStatus('Open the modal first via step 3.', 'bad');
}
// Clear qty (the bug-trigger scenario)
const qtyEl = doc.getElementById('editQty');
qtyEl.value = '';
// Click Save
const saveBtn = doc.querySelector('[data-od-id="edit-save"]');
saveBtn.click();
setTimeout(() => {
const stillOpen = modal.classList.contains('show');
const hintEl = doc.getElementById('editHint');
const hintText = hintEl ? hintEl.textContent : '';
const hintColor = hintEl ? hintEl.style.color : '';
if (stillOpen && /quantity/i.test(hintText) && /red|192, 202/.test(hintColor)) {
setStatusMulti([
{ text: '✓ Edit-line bug FIX verified:', kind: 'ok' },
{ text: ' • Modal stayed open (operator not stuck behind grey backdrop)', kind: 'ok' },
{ text: ' • Inline hint shows "' + hintText + '"', kind: 'ok' },
{ text: ' • Hint is colored red (' + (hintColor || 'unset') + ')', kind: 'ok' },
]);
} else if (stillOpen) {
setStatusMulti([
{ text: '~ Modal stayed open but hint may need adjustment', kind: 'ok' },
{ text: ' • modal still open: ' + stillOpen, kind: 'ok' },
{ text: ' • hint text: "' + hintText + '"', dim: true },
{ text: ' • hint color: "' + (hintColor || 'unset') + '"', dim: true },
]);
} else {
setStatusMulti([
{ text: '✗ Modal CLOSED on invalid input (the original bug)', kind: 'bad' },
{ text: ' The fix is not working as expected. The modal should stay open with an error.', kind: 'bad' },
]);
}
}, 100);
}
function testCancel() {
if (!booted) return setStatus('Boot first.', 'bad');
const doc = iframe.contentDocument;
const modal = doc.getElementById('editSheet');
// Ensure modal is open
if (!modal.classList.contains('show')) {
// Open it first via openEditLine
iframe.contentWindow.__pos.openEditLine('TEST-1');
}
setTimeout(() => {
// Click the Cancel button
const cancelBtn = doc.querySelector('[data-od-id="edit-cancel"]');
cancelBtn.click();
setTimeout(() => {
const stillOpen = modal.classList.contains('show');
if (!stillOpen) {
setStatusMulti([
{ text: '✓ Cancel button closes the modal cleanly', kind: 'ok' },
]);
} else {
setStatusMulti([
{ text: '✗ Cancel did not close the modal', kind: 'bad' },
]);
}
}, 100);
}, 100);
}
</script>
</body>
</html>