[object Object]

← back to NationalPaperHangers

fix: /admin/template flash double-clear (route was reading null after global middleware drained it) — caught by new e2e click-test, all 7 assertions pass

6546f0fc7cdcf3858262bf9f2fe4f33daf0e5e1f · 2026-05-06 16:40:18 -0700 · Steve

Files touched

Diff

commit 6546f0fc7cdcf3858262bf9f2fe4f33daf0e5e1f
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 16:40:18 2026 -0700

    fix: /admin/template flash double-clear (route was reading null after global middleware drained it) — caught by new e2e click-test, all 7 assertions pass
---
 routes/admin.js               |   7 ++-
 tests/e2e-template-chooser.js | 114 ++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 117 insertions(+), 4 deletions(-)

diff --git a/routes/admin.js b/routes/admin.js
index 31b819d..7f98762 100644
--- a/routes/admin.js
+++ b/routes/admin.js
@@ -634,12 +634,11 @@ const TEMPLATE_SLUGS = ['editorial','trade-pro','concierge','studio','heritage',
 
 router.get('/template', async (req, res, next) => {
   try {
-    const flash = req.session.flash;
-    req.session.flash = null;
+    // Flash is captured + cleared by the global middleware in server.js → res.locals.flash.
+    // Don't read req.session.flash here (already null) and don't override locals.
     res.render('admin/template', {
       title: 'Page design · National Paper Hangers',
-      installer: req.installer,
-      flash
+      installer: req.installer
     });
   } catch (err) { next(err); }
 });
diff --git a/tests/e2e-template-chooser.js b/tests/e2e-template-chooser.js
new file mode 100644
index 0000000..18f3510
--- /dev/null
+++ b/tests/e2e-template-chooser.js
@@ -0,0 +1,114 @@
+// e2e click-test: /admin/template chooser, end-to-end.
+//
+// Flow:
+//   1. Login as a paid-tier installer (atelier-bond-nyc / signature)
+//   2. Navigate to /admin/template
+//   3. Pick a non-default template (e.g. "studio")
+//   4. Save
+//   5. Verify the public profile renders with the new template_slug
+//
+// Drag-drop image upload is left as a TODO — it requires a file fixture and
+// adds 30s to the run. Step-1-through-5 covers the critical-path bug surface.
+//
+// Run: node tests/e2e-template-chooser.js
+// Env: BASE=http://localhost:9765 (default)
+
+const { chromium } = require('playwright-core');
+const path = require('path');
+
+const BASE = process.env.BASE || 'http://localhost:9765';
+const EMAIL = 'demo+bond@example.com';
+const PW = 'demo1234';
+const SLUG = 'atelier-bond-nyc';
+const TARGET_TEMPLATE = 'studio';  // pick something different from default 'editorial'
+
+function findChromium() {
+  const paths = [
+    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+    '/Applications/Chromium.app/Contents/MacOS/Chromium',
+    process.env.CHROME_PATH,
+  ].filter(Boolean);
+  for (const p of paths) { try { require('fs').accessSync(p); return p; } catch {} }
+  return null;
+}
+
+(async () => {
+  const exe = findChromium();
+  if (!exe) {
+    console.error('FAIL: no Chromium binary found. Install Google Chrome or set CHROME_PATH.');
+    process.exit(2);
+  }
+  console.log(`[e2e] using browser: ${exe}`);
+  const browser = await chromium.launch({ executablePath: exe, headless: true });
+  const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
+  const page = await ctx.newPage();
+  let pass = 0, fail = 0;
+  const assert = (cond, msg) => { if (cond) { console.log(`  ✓ ${msg}`); pass++; } else { console.log(`  ✗ ${msg}`); fail++; } };
+
+  try {
+    // ─── STEP 1 · login ───────────────────────────────────────────
+    console.log('\n[step 1] login as installer');
+    await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
+    await page.fill('input[name="email"]', EMAIL);
+    await page.fill('input[name="password"]', PW);
+    await Promise.all([
+      page.waitForURL(/\/admin/, { timeout: 8000 }),
+      page.click('form[action="/login"] button[type="submit"]'),
+    ]);
+    assert(page.url().includes('/admin'), `landed on /admin (got ${page.url()})`);
+
+    // ─── STEP 2 · /admin/template ─────────────────────────────────
+    console.log('\n[step 2] open /admin/template');
+    await page.goto(`${BASE}/admin/template`, { waitUntil: 'domcontentloaded' });
+    const cards = await page.$$('.tpl-card');
+    assert(cards.length === 6, `6 template cards present (got ${cards.length})`);
+    const previewIframes = await page.$$('.tpl-card iframe');
+    assert(previewIframes.length === 6, `6 preview iframes (got ${previewIframes.length})`);
+
+    // ─── STEP 3 · pick template ───────────────────────────────────
+    console.log(`\n[step 3] pick template: ${TARGET_TEMPLATE}`);
+    await page.click(`.tpl-card[data-tpl="${TARGET_TEMPLATE}"]`);
+    const stamp = await page.$eval(
+      `.tpl-card[data-tpl="${TARGET_TEMPLATE}"] .stamp`,
+      el => el.textContent.trim()
+    );
+    assert(stamp.includes('Selected'), `selected card shows "Selected" stamp (got "${stamp}")`);
+    const hidden = await page.$eval('#tpl_slug', el => el.value);
+    assert(hidden === TARGET_TEMPLATE, `hidden tpl_slug input = "${TARGET_TEMPLATE}" (got "${hidden}")`);
+
+    // ─── STEP 4 · save ────────────────────────────────────────────
+    console.log('\n[step 4] save form');
+    await Promise.all([
+      page.waitForURL(/\/admin\/template/, { timeout: 8000 }),
+      page.click('#tplForm button[type="submit"]'),
+    ]);
+    const flash = await page.$eval('.callout-success', el => el.textContent.trim()).catch(() => null);
+    assert(flash && /saved/i.test(flash), `success flash shown (got "${flash}")`);
+
+    // ─── STEP 5 · verify public profile uses new template ──────────
+    console.log('\n[step 5] verify public profile renders new template');
+    await page.goto(`${BASE}/installer/${SLUG}`, { waitUntil: 'domcontentloaded' });
+    const bodyClass = await page.$eval('body', el => el.className);
+    assert(
+      bodyClass.includes(`tpl-${TARGET_TEMPLATE}`),
+      `body has class tpl-${TARGET_TEMPLATE} (got "${bodyClass}")`
+    );
+
+    // ─── CLEANUP · revert to editorial ────────────────────────────
+    console.log('\n[cleanup] reverting tpl_slug → editorial');
+    await page.goto(`${BASE}/admin/template`);
+    await page.click('.tpl-card[data-tpl="editorial"]');
+    await Promise.all([
+      page.waitForURL(/\/admin\/template/, { timeout: 8000 }),
+      page.click('#tplForm button[type="submit"]'),
+    ]);
+
+  } catch (err) {
+    console.error('\nFATAL:', err.message);
+    fail++;
+  } finally {
+    await browser.close();
+    console.log(`\n[result] ${pass} pass · ${fail} fail`);
+    process.exit(fail ? 1 : 0);
+  }
+})();

← d15181a Remove 'Schedule install' booking option — initial visit onl  ·  back to NationalPaperHangers  ·  fix(admin/template): drag-drop upload was 403'ing on CSRF — efd7990 →