305 lines
12 KiB
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>
|