screentinker/server/test/directory-board-render.test.js
screentinker 178af029a4
Directory board: JSON/CSV import + logo-replaces-title + fix images on player (#195)
* feat(widgets): bulk import for the directory board (JSON / CSV / TSV / text)

Adds an "Import from JSON / CSV" button to the directory-board editor. Paste JSON
(the { company, tenantsByFloor, advertisements, backgroundImages } shape plus
categories[]/floors[]/flat-array/bare-floor-map variants), a CSV/TSV/pipe/semicolon
table (with or without a header — vacant/yes/1 => available, quoted fields), or a
sectioned "room name" text list, and it auto-fills title, footer, floors->categories,
rooms/names/details/availability, and background-image URLs. "Replace / append" toggle.

Tolerant key matching (room/suite/unit/id, name/tenant/company, details/subtitle, …);
warns on things it can't use (bare-filename background images, headerless columns).
parseDirectoryImport is pure and was unit-tested in node across every format.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(widgets): directory board — logo replaces title, and images load on the player

Two on-screen bugs on the directory board:

1. A logo did not remove the title text — both rendered, stacking the wordmark over
   the name. renderDirectoryBoard (and the directory-search header) now gate the title
   h1 behind !logoSrc, so a logo replaces the title. New render test guards it.

2. Logo + background images did not show on the player (NS_ERROR_DOM_CORP_FAILED,
   0 bytes). The player embeds widgets in a sandbox="allow-scripts" (opaque-origin)
   iframe, so /api/content image requests are cross-origin, and the helmet default
   Cross-Origin-Resource-Policy: same-origin blocks them. Set CORP: cross-origin (+
   ACAO:*) on the content file + thumbnail routes, matching the existing /uploads/content
   static route. Content already serves publicly, so no new exposure. Verified in a real
   sandboxed iframe: same-origin blocks, cross-origin loads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:53:34 -05:00

44 lines
2.3 KiB
JavaScript

'use strict';
// Guards the directory-board header behaviour: a logo REPLACES the title text
// (showing both stacked the wordmark over the name). Renders the public widget
// endpoint and inspects the emitted board script. Mirrors widget-render-xss.test.js.
const test = require('node:test');
const assert = require('node:assert/strict');
const Database = require('better-sqlite3');
process.env.JWT_SECRET = 'test-secret-dir-board';
const db = new Database(':memory:');
db.exec(`CREATE TABLE widgets (id TEXT PRIMARY KEY, widget_type TEXT, config TEXT, workspace_id TEXT);`);
const dbModulePath = require.resolve('../db/database');
require.cache[dbModulePath] = { id: dbModulePath, filename: dbModulePath, loaded: true, exports: { db } };
const express = require('express');
const widgetsRouter = require('../routes/widgets');
const app = express();
app.use('/api/widgets', widgetsRouter);
const server = app.listen(0);
let base;
test.before(async () => { await new Promise(r => server.listening ? r() : server.once('listening', r)); base = `http://127.0.0.1:${server.address().port}`; });
test.after(() => { server.close(); db.close(); });
const seed = (id, config) => db.prepare('INSERT INTO widgets (id, widget_type, config, workspace_id) VALUES (?,?,?,?)').run(id, 'directory-board', JSON.stringify(config), 'ws1');
const render = async (id) => (await fetch(`${base}/api/widgets/${id}/render`)).text();
test('directory board: title text is gated behind !logoSrc (logo replaces title)', async () => {
seed('b1', { title: 'LINNcinnati', logo_url: '/api/content/abc/file', categories: [] });
const html = await render('b1');
// The title h1 must only be appended when there is no logo.
assert.match(html, /if \(cfg\.title && !logoSrc\)/, 'title render must be guarded by !logoSrc');
assert.doesNotMatch(html, /if \(cfg\.title\) \{\s*\n\s*var h1/, 'title must not be rendered unconditionally');
});
test('directory board: still embeds title + logo config for the client', async () => {
seed('b2', { title: 'Lincoln Warehouse', logo_url: '/api/content/xyz/file', categories: [] });
const html = await render('b2');
assert.match(html, /Lincoln Warehouse/, 'title present in embedded config');
assert.match(html, /\/api\/content\/xyz\/file/, 'logo url present in embedded config');
});