← 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
M routes/admin.jsA tests/e2e-template-chooser.js
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 →