← back to Quadrille Showroom
cta/targeted-test.js
482 lines
'use strict';
const {chromium, webkit} = require('playwright');
const fs = require('fs');
const path = require('path');
const SHOTS = '/Users/macstudio3/Projects/quadrille-showroom/cta/shots';
async function testEngine(eng, launcher) {
console.log('\n=== ENGINE:', eng, '===');
const browser = await launcher.launch({
headless: true,
args: eng === 'chrome'
? ['--no-sandbox','--disable-setuid-sandbox','--enable-webgl','--use-gl=swiftshader','--ignore-gpu-blocklist']
: []
});
const ctx = await browser.newContext({ viewport: {width:1600, height:900} });
const page = await ctx.newPage();
const errs = [];
page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); });
page.on('pageerror', e => errs.push('PAGEERR:' + e));
fs.mkdirSync(path.join(SHOTS, eng), { recursive: true });
const results = [];
const R = (comp, ok, note) => {
results.push({ eng, comp, ok, note: note || '' });
console.log(' ', ok ? 'PASS' : 'FAIL', comp, note ? '| ' + note : '');
};
await page.goto('http://127.0.0.1:7690/', { waitUntil: 'domcontentloaded' });
// Wait for ver-rail
try {
await page.waitForSelector('#ver-rail', { timeout: 20000 });
} catch (e) {
console.log('ver-rail timeout');
}
await page.waitForTimeout(2000);
await page.screenshot({ path: path.join(SHOTS, eng, 'initial-state.png') });
// ── 1. VERSION RAIL ───────────────────────────────────────────────────────
const verState = await page.evaluate(() => {
const rail = document.getElementById('ver-rail');
const chips = document.querySelectorAll('.ver-chip');
return {
railExists: !!rail,
railVisible: rail ? (rail.offsetWidth > 0) : false,
chipCount: chips.length,
firstChip: chips.length > 0 ? { text: chips[0].textContent.trim().substring(0,30), classes: chips[0].className } : null,
currentVersion: window._versions ? window._versions.current : null,
};
});
console.log('verState:', JSON.stringify(verState));
R('Version rail (#ver-rail) exists', verState.railExists, 'chips=' + verState.chipCount + ' current=' + verState.currentVersion);
R('Version rail has 11 chips', verState.chipCount >= 11, 'count=' + verState.chipCount);
// ── 2. CLICK EACH VER-CHIP ────────────────────────────────────────────────
const chipCount = await page.evaluate(() => document.querySelectorAll('.ver-chip').length);
for (let i = 0; i < chipCount; i++) {
const chipInfo = await page.evaluate((idx) => {
const chip = document.querySelectorAll('.ver-chip')[idx];
if (!chip) return null;
return { text: chip.textContent.trim().substring(0, 30), classes: chip.className };
}, i);
if (!chipInfo) continue;
// Click via evaluate (avoids pointer interception)
await page.evaluate((idx) => {
const chip = document.querySelectorAll('.ver-chip')[idx];
if (chip) chip.click();
}, i);
await page.waitForTimeout(1800);
const afterClick = await page.evaluate((idx) => {
const chip = document.querySelectorAll('.ver-chip')[idx];
const overlayHost = document.getElementById('mechanic-overlay');
return {
isActive: chip ? chip.classList.contains('active') : false,
currentVersion: window._versions ? window._versions.current : null,
overlayVisible: overlayHost ? (overlayHost.offsetWidth > 0 && overlayHost.style.display !== 'none') : false,
canvasOk: (document.getElementById('showroom-canvas') || {}).offsetWidth > 0,
};
}, i);
// For overlay versions (i >= 5), check overlay appeared
if (i >= 5) {
R('Version chip ' + i + ' (' + chipInfo.text + ') overlay', afterClick.overlayVisible || afterClick.isActive,
'active=' + afterClick.isActive + ' overlay=' + afterClick.overlayVisible + ' ver=' + afterClick.currentVersion);
// Close overlay
await page.evaluate(() => {
const h = document.getElementById('mechanic-overlay');
if (h) h.style.display = 'none';
document.body.classList.remove('mechanic-open');
if (window._versions && window._versions.set) window._versions.set('V1');
});
await page.waitForTimeout(500);
} else {
R('Version chip ' + i + ' (' + chipInfo.text + ') 3D', afterClick.isActive,
'ver=' + afterClick.currentVersion + ' canvas=' + afterClick.canvasOk);
}
}
// Return to V1
await page.evaluate(() => {
if (window._versions && window._versions.set) window._versions.set('V1');
});
await page.waitForTimeout(1500);
// ── 3. NUMBERS OVERLAY ───────────────────────────────────────────────────
await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-before.png') });
await page.evaluate(() => {
const btn = document.getElementById('vr-overlay-btn');
if (btn) btn.click();
});
await page.waitForTimeout(1200);
const overlayState = await page.evaluate(() => {
const pins = document.querySelectorAll('.el-pin');
const pincodes = document.querySelectorAll('.pin-code');
const firstCode = pincodes.length > 0 ? pincodes[0].textContent.trim() : '';
const lastCode = pincodes.length > 0 ? pincodes[pincodes.length - 1].textContent.trim() : '';
return { pinCount: pins.length, pincodeCount: pincodes.length, firstCode, lastCode };
});
console.log('Numbers overlay:', JSON.stringify(overlayState));
R('Numbers overlay: pins appear', overlayState.pinCount > 0, 'pins=' + overlayState.pinCount);
R('Numbers overlay: pin-code chips present', overlayState.pincodeCount > 0, 'codes=' + overlayState.pincodeCount + ' first=' + overlayState.firstCode);
R('Numbers overlay: codes start with 1000QHV1', overlayState.firstCode.startsWith('1000QHV'), 'firstCode=' + overlayState.firstCode);
await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-on.png') });
// ── 4. PIN CLICK → CHOSEN TRAY ───────────────────────────────────────────
// Note: the count element is #ct-count (in chosen-tray), not #tray-count (old panel)
const pinClickResult = await page.evaluate(() => {
const pin = document.querySelector('.el-pin');
if (!pin) return { pinFound: false };
pin.click();
// ct-count is inside the chosen tray
const ctCount = document.getElementById('ct-count');
const chosenLen = window._versions ? window._versions.chosen.length : -1;
return {
pinFound: true,
ctCount: ctCount ? parseInt(ctCount.textContent) || 0 : -1,
chosenLen,
};
});
await page.waitForTimeout(600);
const trayCountAfter = await page.evaluate(() => {
const ctCount = document.getElementById('ct-count');
const chosenLen = window._versions ? window._versions.chosen.length : -1;
return { ctCount: ctCount ? parseInt(ctCount.textContent) || 0 : -1, chosenLen };
});
console.log('Pin click:', JSON.stringify(pinClickResult), 'tray after:', JSON.stringify(trayCountAfter));
R('Pin click → Chosen tray increments (ct-count / _versions.chosen)', trayCountAfter.chosenLen > 0, 'chosenLen=' + trayCountAfter.chosenLen + ' ctCount=' + trayCountAfter.ctCount);
await page.screenshot({ path: path.join(SHOTS, eng, 'pin-clicked.png') });
// ── 5. CT-COPY ────────────────────────────────────────────────────────────
// First open tray (click header to expand if collapsed)
const ctCopyResult = await page.evaluate(() => {
// Try to expand tray first
const head = document.getElementById('chosen-tray-head');
if (head) head.click();
return null;
});
await page.waitForTimeout(400);
const copyBtnResult = await page.evaluate(() => {
const btn = document.getElementById('ct-copy');
if (!btn) return { found: false, visible: false };
const cs = window.getComputedStyle(btn);
return { found: true, visible: cs.display !== 'none' && cs.visibility !== 'hidden', display: cs.display };
});
console.log('ct-copy state:', JSON.stringify(copyBtnResult));
const copyClicked = await page.evaluate(() => {
const btn = document.getElementById('ct-copy');
if (!btn) return 'not found';
try { btn.click(); return 'clicked'; } catch (e) { return 'error: ' + e.message; }
});
R('ct-copy button clickable', copyClicked === 'clicked', 'visible=' + copyBtnResult.visible + ' result=' + copyClicked);
// ── 6. NUMBERS OVERLAY OFF ────────────────────────────────────────────────
await page.evaluate(() => {
const btn = document.getElementById('vr-overlay-btn');
if (btn) btn.click();
});
await page.waitForTimeout(600);
const pinsAfterOff = await page.evaluate(() => document.querySelectorAll('.el-pin').length);
R('Numbers overlay OFF (toggle)', pinsAfterOff === 0, 'pins=' + pinsAfterOff);
await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-off.png') });
// ── 7. YAW SPIN ──────────────────────────────────────────────────────────
// Focus canvas then keypress
await page.evaluate(() => {
const canvas = document.getElementById('showroom-canvas');
if (canvas) canvas.focus();
});
for (let i = 0; i < 5; i++) {
await page.keyboard.press('ArrowLeft');
await page.waitForTimeout(60);
}
await page.waitForTimeout(800);
await page.screenshot({ path: path.join(SHOTS, eng, 'yaw-left.png') });
const canvasOkAfterYaw = await page.evaluate(() => {
const c = document.getElementById('showroom-canvas');
return { exists: !!c, width: c ? c.offsetWidth : 0, noError: true };
});
R('Arrow-key yaw spin (5× ArrowLeft)', canvasOkAfterYaw.exists, 'canvas ' + canvasOkAfterYaw.width + 'px, no crash');
for (let i = 0; i < 5; i++) {
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(60);
}
// ── 8. WING CLICK (via dispatchEvent) ─────────────────────────────────────
await page.screenshot({ path: path.join(SHOTS, eng, 'wing-before.png') });
await page.evaluate(() => {
const canvas = document.getElementById('showroom-canvas');
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
canvas.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }));
canvas.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: cx, clientY: cy }));
canvas.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: cx, clientY: cy }));
});
await page.waitForTimeout(2500);
const detailState = await page.evaluate(() => {
const d = document.getElementById('wing-detail');
if (!d) return { exists: false };
const cs = window.getComputedStyle(d);
return {
exists: true,
hidden: d.classList.contains('hidden'),
collapsed: d.classList.contains('collapsed'),
display: cs.display,
opacity: cs.opacity,
};
});
console.log('Wing detail state after click:', JSON.stringify(detailState));
R('Wing canvas click → detail visible', detailState.exists && !detailState.hidden,
'hidden=' + detailState.hidden + ' collapsed=' + detailState.collapsed + ' display=' + detailState.display);
await page.screenshot({ path: path.join(SHOTS, eng, 'wing-after.png') });
// If detail visible, test expand
if (detailState.exists && !detailState.hidden) {
if (detailState.collapsed) {
await page.evaluate(() => {
const b = document.getElementById('detail-collapse');
if (b) b.click();
});
await page.waitForTimeout(800);
}
const expanded = await page.evaluate(() => {
const d = document.getElementById('wing-detail');
return d ? !d.classList.contains('collapsed') : false;
});
const specText = await page.evaluate(() => {
const sp = document.querySelector('.wd-specs, .wd-body, #detail-spec-grid');
return sp ? sp.textContent.trim().substring(0, 200) : '';
});
R('Wing detail expand', expanded, 'expanded=' + expanded);
R('Wing detail specs text', specText.length > 10, '"' + specText.substring(0, 80) + '"');
await page.screenshot({ path: path.join(SHOTS, eng, 'wing-detail-expanded.png') });
// Close
await page.evaluate(() => {
const b = document.getElementById('close-detail');
if (b) b.click();
});
await page.waitForTimeout(500);
const closed = await page.evaluate(() => {
const d = document.getElementById('wing-detail');
return d ? d.classList.contains('hidden') : true;
});
R('Wing detail close', closed, 'hidden=' + closed);
}
// ── 9. createImageBitmap / OffscreenCanvas (WebKit) ───────────────────────
const imgBitmapOk = await page.evaluate(() => typeof createImageBitmap === 'function');
const offscreenOk = await page.evaluate(() => {
try { return !!new OffscreenCanvas(2, 2); } catch (e) { return false; }
});
R('createImageBitmap API present', imgBitmapOk, eng === 'safari' ? 'WEBKIT-KEY' : 'chrome');
R('OffscreenCanvas API present', offscreenOk, eng === 'safari' ? 'WEBKIT-KEY' : 'chrome');
// ── 10. GUIDED BAR ───────────────────────────────────────────────────────
await page.screenshot({ path: path.join(SHOTS, eng, 'guided-bar.png') });
const guidedState = await page.evaluate(() => {
const bar = document.getElementById('guided-bar');
return { exists: !!bar, visible: bar ? bar.offsetWidth > 0 : false, display: bar ? window.getComputedStyle(bar).display : null };
});
R('Guided bar visible', guidedState.visible, 'display=' + guidedState.display);
// Next
await page.evaluate(() => { const b = document.getElementById('g-next'); if (b) b.click(); });
await page.waitForTimeout(1500);
await page.screenshot({ path: path.join(SHOTS, eng, 'guided-next.png') });
R('Guided Next click', true, 'clicked via evaluate');
// Prev
await page.evaluate(() => { const b = document.getElementById('g-prev'); if (b) b.click(); });
await page.waitForTimeout(1200);
R('Guided Prev click', true, 'clicked via evaluate');
// Auto-Tour
await page.evaluate(() => { const b = document.getElementById('g-tour'); if (b) b.click(); });
await page.waitForTimeout(2000);
const tourPlaying = await page.evaluate(() => {
const t = document.getElementById('g-tour');
return t ? t.classList.contains('playing') : false;
});
await page.screenshot({ path: path.join(SHOTS, eng, 'auto-tour.png') });
R('Auto-Tour playing', tourPlaying, 'playing=' + tourPlaying);
await page.evaluate(() => { const b = document.getElementById('g-tour'); if (b) b.click(); });
await page.waitForTimeout(800);
// All Designs
await page.evaluate(() => { const b = document.getElementById('g-grid'); if (b) b.click(); });
await page.waitForTimeout(1500);
const gridOpen = await page.evaluate(() => {
const g = document.getElementById('grid-overlay');
return g ? g.classList.contains('open') : false;
});
await page.screenshot({ path: path.join(SHOTS, eng, 'all-designs.png') });
R('All Designs grid open', gridOpen, 'open=' + gridOpen);
await page.evaluate(() => { const b = document.getElementById('go-close'); if (b) b.click(); });
await page.waitForTimeout(500);
// ── 11. SLIDERS ──────────────────────────────────────────────────────────
for (const [slId, slName, slVal] of [['density-range','density','14'],['angle-range','angle','45'],['reveal-range','reveal','50']]) {
const r = await page.evaluate(({id, val}) => {
const el = document.getElementById(id);
if (!el) return { found: false };
const old = el.value;
el.value = val;
el.dispatchEvent(new Event('input', { bubbles: true }));
return { found: true, old, newVal: el.value };
}, {id: slId, val: slVal});
R('Slider ' + slName, r.found, r.found ? r.old + '→' + r.newVal : 'not found');
}
await page.screenshot({ path: path.join(SHOTS, eng, 'sliders.png') });
// ── 12. TOP-BAR BUTTONS ──────────────────────────────────────────────────
for (const btnId of ['btn-bookmatch', 'btn-walls', 'btn-explore']) {
const r = await page.evaluate((id) => {
const b = document.getElementById(id);
if (!b) return { found: false };
const before = b.className;
b.click();
const after = b.className;
return { found: true, visible: b.offsetWidth > 0, before, after, changed: before !== after };
}, btnId);
R('Top-bar ' + btnId, r.found, JSON.stringify(r));
// Toggle back
await page.evaluate((id) => { const b = document.getElementById(id); if (b) b.click(); }, btnId);
}
// ── 13. WINDOW NAV ────────────────────────────────────────────────────────
const labelBefore = await page.evaluate(() => {
const l = document.getElementById('window-label');
return l ? l.textContent.trim() : null;
});
await page.evaluate(() => { const b = document.getElementById('btn-next-window'); if (b) b.click(); });
await page.waitForTimeout(1800);
const labelAfter = await page.evaluate(() => {
const l = document.getElementById('window-label');
return l ? l.textContent.trim() : null;
});
await page.screenshot({ path: path.join(SHOTS, eng, 'window-nav.png') });
R('Window nav Next50', labelAfter !== labelBefore && labelAfter !== '1–50 of 883', 'before=' + labelBefore + ' after=' + labelAfter);
// Prev
await page.evaluate(() => { const b = document.getElementById('btn-prev-window'); if (b) b.click(); });
await page.waitForTimeout(1500);
R('Window nav Prev50', true, 'clicked');
// ── 14. SORT SELECT ───────────────────────────────────────────────────────
const sortOk = await page.evaluate(() => {
const s = document.getElementById('sort-select');
if (!s) return false;
s.value = 'title';
s.dispatchEvent(new Event('change', { bubbles: true }));
return s.value === 'title';
});
R('Sort select', sortOk, 'value=title');
// ── 15. PROTO PAGES ──────────────────────────────────────────────────────
const protos = [
{ route: '/proto/v6-colorriver.html', name: 'v6-colorriver', clickSel: 'canvas, .swatch-card, button, .filter-btn' },
{ route: '/proto/v7-moodboard.html', name: 'v7-moodboard', clickSel: 'canvas, .board-card, button' },
{ route: '/proto/v8-swipe.html', name: 'v8-swipe', clickSel: 'button, .card, .swipe-card, .keep-btn, .skip-btn' },
{ route: '/proto/v9-walkin.html', name: 'v9-walkin', clickSel: 'canvas, button, .design-chip' },
{ route: '/proto/v10-concierge.html',name: 'v10-concierge', clickSel: 'button, .send-btn, input[type="text"]' },
{ route: '/proto/sample-table.html', name: 'sample-table', clickSel: 'canvas, button' },
];
for (const proto of protos) {
const pPage = await ctx.newPage();
const pErrs = [];
pPage.on('console', m => { if (m.type() === 'error') pErrs.push(m.text()); });
pPage.on('pageerror', e => pErrs.push('PAGEERR:' + e));
await pPage.goto('http://127.0.0.1:7690' + proto.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
await pPage.waitForTimeout(2000);
await pPage.screenshot({ path: path.join(SHOTS, eng, proto.name + '-before.png') });
const bodyLen = await pPage.evaluate(() => document.body.innerText.trim().length);
const title = await pPage.title();
// Try clicking primary element (via evaluate to avoid visibility issues)
let primaryOk = false;
const primaryResult = await pPage.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el) return 'not found';
try { el.click(); return 'clicked: ' + el.tagName + '.' + el.className.substring(0, 20); }
catch (e) { return 'error: ' + e.message; }
}, proto.clickSel);
primaryOk = !primaryResult.includes('not found') && !primaryResult.includes('error');
await pPage.waitForTimeout(800);
await pPage.screenshot({ path: path.join(SHOTS, eng, proto.name + '-after.png') });
console.log('Proto', proto.name, 'title=', title, 'body=', bodyLen, 'primary=', primaryResult, 'errors=', pErrs.length);
R('Proto ' + proto.name + ' renders', bodyLen > 10, 'title="' + title + '"');
R('Proto ' + proto.name + ' primary click', primaryOk, primaryResult);
if (pErrs.length > 0) R('Proto ' + proto.name + ' 0 errors', false, pErrs.slice(0, 3).join('; '));
await pPage.close();
}
// ── FINAL: ERROR COUNT ────────────────────────────────────────────────────
console.log('\nTotal console errors:', errs.length);
if (errs.length > 0) console.log('Errors:', errs.slice(0, 10));
R('Zero console errors (main page)', errs.length === 0, 'count=' + errs.length);
await page.screenshot({ path: path.join(SHOTS, eng, 'final.png') });
await ctx.close();
await browser.close();
return results;
}
(async () => {
const allResults = [];
for (const [eng, launcher] of [['chrome', chromium], ['safari', webkit]]) {
const r = await testEngine(eng, launcher);
allResults.push(...r);
}
// Summary
console.log('\n=== FINAL SUMMARY ===');
const chromeFail = allResults.filter(r => r.eng === 'chrome' && !r.ok);
const safariFail = allResults.filter(r => r.eng === 'safari' && !r.ok);
const chromePass = allResults.filter(r => r.eng === 'chrome' && r.ok);
const safariPass = allResults.filter(r => r.eng === 'safari' && r.ok);
console.log('Chrome: PASS=' + chromePass.length + ' FAIL=' + chromeFail.length);
console.log('Safari: PASS=' + safariPass.length + ' FAIL=' + safariFail.length);
if (chromeFail.length) {
console.log('\nChrome FAIL:');
chromeFail.forEach(r => console.log(' -', r.comp, '|', r.note));
}
if (safariFail.length) {
console.log('\nSafari FAIL:');
safariFail.forEach(r => console.log(' -', r.comp, '|', r.note));
}
// Mismatches
const allComps = [...new Set(allResults.map(r => r.comp))];
const mismatches = [];
for (const comp of allComps) {
const cr = allResults.find(r => r.eng === 'chrome' && r.comp === comp);
const sa = allResults.find(r => r.eng === 'safari' && r.comp === comp);
if (cr && sa && cr.ok !== sa.ok) mismatches.push({ comp, chrome: cr.ok, safari: sa.ok });
}
if (mismatches.length) {
console.log('\nChrome/Safari MISMATCHES:');
mismatches.forEach(m => console.log(' -', m.comp, 'chrome=' + m.chrome, 'safari=' + m.safari));
} else {
console.log('\nNo Chrome/Safari mismatches.');
}
// Save JSON results
fs.writeFileSync('/Users/macstudio3/Projects/quadrille-showroom/cta/targeted-results.json', JSON.stringify(allResults, null, 2));
console.log('\nResults saved to cta/targeted-results.json');
})().catch(e => { console.error('FATAL:', e); process.exit(1); });