import { test, beforeEach } from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { RESERVED_KEYS, registerEmailBlock, getEmailBlock, listEmailBlocks, newEmailBlock, } from '../src/emailBlocks/registry.js' // Engagement Phase 5b — the client half of the template editor. // // Two kinds of test, and the second kind is the one worth explaining. // // `registry.js` is plain `.js` and imports nothing, so it is exercised directly. // `types.jsx` and `EngagementTemplates.jsx` cannot be: this runner has no JSX // transform and no DOM, the same limit `moduleRegistry.test.js` documents. So the // properties that live in those files are asserted **against their source text**. // // That is a weaker test than executing them, and it is used for exactly two things // where a weak test still beats none: // // • **The preview sandbox.** `sandbox=""` with no `allow-scripts` is the reason // operator-authored HTML cannot run under this site's origin. It is one // attribute, on one element, and it is precisely the sort of thing someone // removes to debug a rendering problem and does not put back. A source // assertion catches that in review; nothing else here would. // • **Registry drift.** Every `email.*` type this client offers must exist in // the server registry with the same version, because the server validates // against its own and a drifted client produces a refused save with no // explanation on screen. Reading both trees is the only way to check a // pairing that spans a process boundary. const here = path.dirname(fileURLToPath(import.meta.url)) const read = (rel) => fs.readFileSync(path.join(here, '..', rel), 'utf8') // The registry is module state; each test starts from a known entry. beforeEach(() => { if (!getEmailBlock('email.test')) { registerEmailBlock({ type: 'email.test', version: 2, label: 'Test block', defaults: () => ({ text: 'hi' }), editor: () => null, }) } }) // ── The registry ─────────────────────────────────────────────────────────── test('a definition must be namespaced "email."', () => { assert.throws(() => registerEmailBlock({ type: 'heading' }), /namespaced/) assert.throws(() => registerEmailBlock({}), /namespaced/) }) test('a duplicate type is a programmer error, caught at import', () => { assert.throws(() => registerEmailBlock({ type: 'email.test' }), /already registered/) }) test('a new block carries the envelope the server expects, and a unique id', () => { const a = newEmailBlock('email.test') const b = newEmailBlock('email.test') assert.deepEqual(Object.keys(a).sort(), [...RESERVED_KEYS].sort()) assert.equal(a.type, 'email.test') assert.equal(a.version, 2) assert.deepEqual(a.props, { text: 'hi' }) // Ids are unique across a whole document. A counter would re-issue an id after // a delete and the save would be refused for a reason nothing on screen explains. assert.notEqual(a.id, b.id) }) test('an unknown type yields nothing rather than a half-built block', () => { assert.equal(newEmailBlock('email.nope'), null) assert.equal(getEmailBlock('email.nope'), null) }) // ── The sandbox: §4.6.2's security posture, as an attribute ──────────────── test('the preview frame is sandboxed with no allow-scripts', () => { const source = read('src/routes/admin/views/EngagementTemplates.jsx') // It renders in an iframe at all — not into the page. assert.match(source, /