← back to Dw Pairs Well
5x/clickthrough.mjs
121 lines
#!/usr/bin/env node
// pairs-well clickthrough — exercises the REAL preview.html controls (the shared
// 3x clickthrough.js is hardcoded for the wallco curator UI and can't test this app).
// Asserts: sample-SKU buttons populate the coordinate grid, #sku+#load loads a product,
// paint-brand checkboxes toggle without errors, every rendered product link is a
// /products/<handle> URL, and ZERO console/page errors throughout.
// Usage: node 5x/clickthrough.mjs [--url http://127.0.0.1:9821/preview.html]
import path from 'node:path';
import os from 'node:os';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const URL_ = process.argv.includes('--url')
? process.argv[process.argv.indexOf('--url') + 1]
: 'http://127.0.0.1:9821/preview.html';
function resolvePlaywright() {
for (const c of [
path.join(os.homedir(), 'Projects/Designer-Wallcoverings/node_modules/playwright'),
path.join(os.homedir(), 'node_modules/playwright'), 'playwright',
]) { try { return require(c); } catch {} }
return null;
}
const pw = resolvePlaywright();
if (!pw) { console.error('playwright not installed'); process.exit(2); }
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
let passed = 0, failed = 0;
const errors = [];
const browser = await pw.chromium.launch({ executablePath: CHROME, headless: true });
const page = await (await browser.newContext()).newPage();
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push(String(e)));
async function step(name, fn) {
const before = errors.length;
try {
await fn();
if (errors.length > before) throw new Error(`console error: ${errors[errors.length - 1]}`.slice(0, 160));
console.log(` [PASS] ${name}`); passed++;
} catch (e) {
console.log(` [FAIL] ${name} — ${String(e.message || e).slice(0, 140)}`); failed++;
}
}
// Expand the Design-Coordinate CTA panel (collapsed by default on source change)
// then wait for the grid to fill with real product links/images.
async function expandAndWaitForGrid() {
const cta = page.locator('#pairs-well-with .pairs-well__cta');
if ((await cta.getAttribute('aria-expanded')) !== 'true') await cta.click();
await page.waitForFunction(() => {
const g = document.getElementById('catalog-grid');
return g && !g.hidden && g.querySelectorAll('a[href*="/products/"], img').length > 0;
}, null, { timeout: 25000 });
}
console.log(`\npairs-well clickthrough → ${URL_}\n`);
await page.goto(URL_, { waitUntil: 'domcontentloaded', timeout: 20000 });
await step('page load, no console errors', async () => {
await page.waitForTimeout(1500);
});
await step('sample SKU chip + Design-Coordinate CTA expand → grid populates', async () => {
await page.locator('button[data-sku]').first().click();
await expandAndWaitForGrid();
});
await step('all rendered product links are /products/<handle>', async () => {
const bad = await page.$$eval('#pairs-well-with a[href]', as =>
as.map(a => a.getAttribute('href')).filter(h => h && !/\/products\/[a-z0-9]/i.test(h)));
if (bad.length) throw new Error(`non-product hrefs: ${bad.slice(0, 3).join(', ')}`);
});
await step('#sku + Load → grid reloads for typed SKU', async () => {
await page.fill('#sku', 'BGA-47951');
await page.locator('#load').click();
await page.waitForFunction(() => {
const t = document.querySelector('#product-title');
return t && t.textContent && t.textContent.trim().length > 3;
}, null, { timeout: 25000 });
});
await step('paint-brand checkbox toggles cleanly', async () => {
const cb = page.locator('input[type=checkbox][value=sw]');
await cb.click(); await page.waitForTimeout(400); await cb.click();
});
await step('second sample chip → grid swaps with real cards', async () => {
const prevTitle = await page.locator('#product-title').textContent();
await page.locator('button[data-sku]').nth(3).click();
await page.waitForFunction(prev => {
const t = document.querySelector('#product-title');
return t && t.textContent.trim().length > 3 && t.textContent !== prev;
}, prevTitle, { timeout: 25000 });
// loadSource collapses the Design-Coordinate panel for the new source — re-expand
await expandAndWaitForGrid();
});
await step('unknown SKU → graceful error state, no page exceptions', async () => {
const before = errors.length;
await page.fill('#sku', 'NOPE-0000000');
await page.locator('#load').click();
await page.waitForTimeout(2500);
// The page DESIGNEDLY console.errors its fetch failures ("[preview] … http 404",
// resource-load 404s). Those are the graceful path, not defects. Real exceptions
// (pageerror / anything else) still fail this step.
const fresh = errors.slice(before);
const unexpected = fresh.filter(e =>
!/http 404|Failed to load resource.*404|\[preview\].*fetch failed/i.test(e));
errors.length = before; // absorb the expected noise
if (unexpected.length) throw new Error(`unexpected error: ${unexpected[0]}`.slice(0, 150));
});
await browser.close();
console.log(`\n CLICK-THROUGH → ${passed} passed, ${failed} failed · ${errors.length} console/page errors total`);
if (errors.length) console.log(' errors: ' + errors.slice(0, 4).join(' | ').slice(0, 300));
process.exit(failed ? 1 : 0);