Files
worldmonitor/tests/brief-magazine-render.test.mjs
Elie Habib 89c179e412 fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time (#3254)
* fix(brief): cover greeting was hardcoded "Good evening" regardless of issue time

Reported: a brief viewed at 13:02 local time showed "Good evening" on the
cover (slide 1) but "Good afternoon." on the digest greeting page (slide 2).

Cause: `server/_shared/brief-render.js:renderCover` had the string
`'Good evening'` hardcoded in the cover's mono-cased salutation slot.
The digest greeting page (slide 2) renders the time-of-day-correct
value from `envelope.data.digest.greeting`, which is computed by
`shared/brief-filter.js:174-179` from `localHour` in the user's TZ
(< 12 → morning, < 18 → afternoon, else → evening). So any brief
viewed outside the literal evening showed an inconsistent pair.

Fix: thread `digest.greeting` into `renderCover`; a small
`coverGreeting()` helper strips the trailing period so the cover's
no-punctuation mono style is preserved. On unexpected/missing values
it falls back to a generic "Hello" rather than silently re-hardcoding
a specific time of day.

Tests: 5 regression cases in `tests/brief-magazine-render.test.mjs`
cover afternoon/morning/evening parity, period stripping, and HTML
escape (defense-in-depth). 60 total in that file pass. Full
test:data 5921 pass. typecheck + typecheck:api + biome clean.

* chore(brief): fix orphaned JSDoc on coverGreeting / renderCover

Greptile flagged: the original `renderCover` JSDoc block stayed above
`coverGreeting` when the helper was inserted, so the @param shape was
misattributed to the wrong function and `renderCover` was left
undocumented (plus the new `greeting` field was unlisted).

Moved the opts-shape JSDoc to immediately above `renderCover` and
added `greeting: string` to the param type. `coverGreeting` keeps its
own prose comment.

No runtime change.
2026-04-21 13:46:21 +04:00

767 lines
32 KiB
JavaScript

// Shape tests for the deterministic brief magazine renderer.
//
// The renderer is pure — same envelope in, same HTML out. These tests
// pin down the page-sequence rules that the rest of the pipeline
// (edge route, dashboard panel, email teaser, carousel, Tauri reader)
// depends on. If one of these breaks, every consumer gets confused.
//
// The forbidden-field guard protects the invariant that the renderer
// only ever interpolates `envelope.data.*` fields. We prove this two
// ways: (1) assert known field-name TOKENS (JSON keys like
// `"importanceScore":`) never appear in the output, and (2) inject
// sentinels into non-`data` locations of the envelope and assert the
// sentinels are absent. The earlier version of this test matched bare
// substrings like "openai" / "claude" / "gemini", which false-fails
// on any legitimate story covering those companies.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { renderBriefMagazine } from '../server/_shared/brief-render.js';
import { BRIEF_ENVELOPE_VERSION } from '../shared/brief-envelope.js';
/**
* @typedef {import('../shared/brief-envelope.js').BriefEnvelope} BriefEnvelope
* @typedef {import('../shared/brief-envelope.js').BriefStory} BriefStory
* @typedef {import('../shared/brief-envelope.js').BriefThread} BriefThread
*/
// ── Fixtures ─────────────────────────────────────────────────────────────────
/** @returns {BriefStory} */
function story(overrides = {}) {
return {
category: 'Energy',
country: 'IR',
threatLevel: 'high',
headline: 'Iran declares Strait of Hormuz open. Oil drops more than 9%.',
description:
'Tehran publicly reopened the Strait of Hormuz to commercial shipping today.',
source: 'Multiple wires',
sourceUrl: 'https://example.com/hormuz-open',
whyMatters:
'Hormuz is roughly a fifth of global seaborne oil — a 9% move in a single session is a repricing, not a wobble.',
...overrides,
};
}
/** @returns {BriefThread} */
function thread(tag, teaser) {
return { tag, teaser };
}
/**
* @param {Partial<import('../shared/brief-envelope.js').BriefData>} overrides
* @returns {BriefEnvelope}
*/
function envelope(overrides = {}) {
const data = {
user: { name: 'Elie', tz: 'UTC' },
issue: '17.04',
date: '2026-04-17',
dateLong: '17 April 2026',
digest: {
greeting: 'Good evening.',
lead: 'The most impactful development today is the reopening of the Strait of Hormuz.',
numbers: { clusters: 278, multiSource: 21, surfaced: 4 },
threads: [
thread('Energy', 'Iran reopens the Strait of Hormuz.'),
thread('Diplomacy', 'Israel\u2013Lebanon ceasefire takes effect.'),
thread('Maritime', 'US military expands posture against Iran-linked shipping.'),
thread('Humanitarian', 'A record year at sea for Rohingya refugees.'),
],
signals: [
'Adherence to the Israel\u2013Lebanon ceasefire in the first 72 hours.',
'Long-term stability of commercial shipping through Hormuz.',
],
},
stories: [
story(),
story({ country: 'IL', category: 'Diplomacy' }),
story({ country: 'US', category: 'Maritime', threatLevel: 'critical' }),
story({ country: 'MM', category: 'Humanitarian' }),
],
...overrides,
};
return {
version: BRIEF_ENVELOPE_VERSION,
issuedAt: 1_700_000_000_000,
data,
};
}
// ── Helpers ──────────────────────────────────────────────────────────────────
/** @param {string} html */
function pageCount(html) {
const matches = html.match(/<section class="page/g);
return matches ? matches.length : 0;
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe('renderBriefMagazine — page sequence', () => {
it('default case: cover + 4 digest pages + N stories + back cover = N + 6', () => {
const env = envelope();
const html = renderBriefMagazine(env);
assert.equal(pageCount(html), env.data.stories.length + 6);
});
it('omits the Signals page when signals is empty', () => {
const env = envelope({
digest: {
...envelope().data.digest,
signals: [],
},
});
const html = renderBriefMagazine(env);
// cover + greeting + numbers + threads + N stories + back = N + 5
assert.equal(pageCount(html), env.data.stories.length + 5);
assert.ok(!html.includes('Digest / 04'), 'Signals page label should not appear');
assert.ok(!html.includes('Signals To Watch'), 'Signals heading should not appear');
});
it('splits On The Desk into 03a + 03b when threads.length > 6', () => {
const manyThreads = Array.from({ length: 8 }, (_, i) =>
thread(`Tag${i}`, `Teaser number ${i}.`),
);
const env = envelope({
digest: { ...envelope().data.digest, threads: manyThreads },
});
const html = renderBriefMagazine(env);
assert.ok(html.includes('Digest / 03a'), 'must emit 03a label');
assert.ok(html.includes('Digest / 03b'), 'must emit 03b label');
assert.ok(!html.includes('Digest / 03 \u2014 On The Desk'), 'must not emit the single-page label');
// cover + greeting + numbers + 03a + 03b + signals + N stories + back = N + 7
assert.equal(pageCount(html), env.data.stories.length + 7);
});
it('splits On The Desk even when signals is empty (still two threads pages)', () => {
const manyThreads = Array.from({ length: 10 }, (_, i) =>
thread(`Tag${i}`, `Teaser ${i}.`),
);
const env = envelope({
digest: { ...envelope().data.digest, threads: manyThreads, signals: [] },
});
const html = renderBriefMagazine(env);
// cover + greeting + numbers + 03a + 03b + N stories + back = N + 6
assert.equal(pageCount(html), env.data.stories.length + 6);
assert.ok(html.includes('03a'));
assert.ok(html.includes('03b'));
});
it('alternates story palette starting with light', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const storyMatches = [...html.matchAll(/<section class="page story (light|dark)"/g)];
assert.equal(storyMatches.length, env.data.stories.length);
storyMatches.forEach((m, i) => {
const expected = i % 2 === 0 ? 'light' : 'dark';
assert.equal(m[1], expected, `story ${i + 1} palette`);
});
});
it('zero-pads the surfaced stat and story rank numbers', () => {
const env = envelope({
digest: { ...envelope().data.digest, numbers: { clusters: 5, multiSource: 2, surfaced: 4 } },
});
const html = renderBriefMagazine(env);
assert.ok(html.includes('<div class="stat-num">04</div>'));
assert.ok(html.includes('<div class="rank-ghost">01</div>'));
assert.ok(html.includes('<div class="rank-ghost">04</div>'));
});
});
describe('renderBriefMagazine — chrome invariants', () => {
it('logo symbol is emitted exactly once; all placements reference it via <use>', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const symbolDefs = html.match(/<symbol id="wm-logo-core"/g) || [];
assert.equal(symbolDefs.length, 1, 'exactly one symbol definition');
// 1 cover + 4 digest pages + N story chromes + 1 back cover = N + 6 logo references
const useRefs = html.match(/<use href="#wm-logo-core"\s*\/>/g) || [];
const expected = 1 + 4 + env.data.stories.length + 1;
assert.equal(useRefs.length, expected);
// Every reference still carries the aria label for a11y.
const ariaLabels = html.match(/aria-label="WorldMonitor"/g) || [];
assert.equal(ariaLabels.length, expected);
});
it('every page is full-bleed (100vw / 100vh declared in the shared stylesheet)', () => {
const html = renderBriefMagazine(envelope());
assert.ok(/\.page\s*\{[^}]*flex:\s*0\s*0\s*100vw/.test(html));
assert.ok(/\.page\s*\{[^}]*height:\s*100vh/.test(html));
});
it('emits the dot-navigation container and digest-index dataset', () => {
const html = renderBriefMagazine(envelope());
assert.ok(html.includes('id="navDots"'));
const m = html.match(/data-digest-indexes='(\[[^']+\])'/);
assert.ok(m, 'deck must expose digest indexes to nav script');
const arr = JSON.parse(m[1]);
assert.ok(Array.isArray(arr));
assert.equal(arr.length, 4, 'default envelope has 4 digest pages');
assert.ok(arr.every((n) => typeof n === 'number'), 'digest indexes are numbers only');
});
it('each story page has a three-tag row (category, country, threat level)', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const tagRows = html.match(/<div class="tag-row">([\s\S]*?)<\/div>\s*<h3/g) || [];
assert.equal(tagRows.length, env.data.stories.length);
for (const row of tagRows) {
const tags = row.match(/<span class="tag[^"]*">/g) || [];
assert.equal(tags.length, 3, `expected 3 tags, got ${tags.length} in ${row}`);
}
});
it('page numbers are 1-indexed and count up to the total', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const total = pageCount(html);
const nums = [...html.matchAll(/<div class="page-number mono">(\d{2}) \/ (\d{2})<\/div>/g)];
assert.equal(nums.length, total);
nums.forEach((m, i) => {
assert.equal(Number(m[1]), i + 1);
assert.equal(Number(m[2]), total);
});
});
it('applies .crit highlight to critical and high threat levels only', () => {
const env = envelope({
stories: [
story({ threatLevel: 'critical' }),
story({ threatLevel: 'high' }),
story({ threatLevel: 'medium' }),
story({ threatLevel: 'low' }),
],
});
const html = renderBriefMagazine(env);
// "Critical" and "High" tags get the .crit class; "Medium" and "Low" do not.
assert.ok(html.includes('<span class="tag crit">Critical</span>'));
assert.ok(html.includes('<span class="tag crit">High</span>'));
assert.ok(html.includes('<span class="tag">Medium</span>'));
assert.ok(html.includes('<span class="tag">Low</span>'));
});
});
describe('renderBriefMagazine — envelope internals never leak into HTML', () => {
// Structural invariant: the renderer only reads `envelope.data.*`.
// We verify this two ways: (1) field-name tokens that only appear in
// upstream seed data (importanceScore, etc.) never leak; (2) sentinel
// values injected into non-data envelope locations are absent from
// the output.
it('does not emit upstream seed field-name tokens as JSON keys or bare names', () => {
const env = envelope();
const html = renderBriefMagazine(env);
// Field-name tokens — these are structural keys that would only
// appear if the renderer accidentally interpolated an envelope
// object (e.g. JSON.stringify(envelope)). Free-text content
// cannot plausibly emit `"importanceScore":` or `_seed`.
const forbiddenKeys = [
'"importanceScore"',
'"primaryLink"',
'"pubDate"',
'"generatedAt"',
'"briefModel"',
'"briefProvider"',
'"fetchedAt"',
'"recordCount"',
'"_seed"',
];
for (const token of forbiddenKeys) {
assert.ok(!html.includes(token), `forbidden token ${token} appeared in HTML`);
}
});
it('validator rejects extension fields on envelope root (importanceScore, _seed, etc.)', () => {
// Stricter than "renderer does not interpolate them". Forbidden
// fields must be impossible to PERSIST in the envelope at all —
// the renderer runs after they are already written to Redis, so
// the only place the invariant can live is the validator at
// write + read time.
const env = /** @type {any} */ ({
...envelope(),
importanceScore: 999,
primaryLink: 'https://example.com',
pubDate: 123,
_seed: { version: 1, fetchedAt: 0 },
});
assert.throws(() => renderBriefMagazine(env), /envelope has unexpected key/);
});
it('HTML-escapes user-provided content (no raw angle brackets from stories)', () => {
const env = envelope({
stories: [
story({
headline: 'Something with <script>alert(1)</script> in it',
whyMatters: 'Why matters with <img src=x> attempt',
}),
...envelope().data.stories.slice(1),
],
});
const html = renderBriefMagazine(env);
assert.ok(!html.includes('<script>alert(1)</script>'));
assert.ok(!html.includes('<img src=x>'));
assert.ok(html.includes('&lt;script&gt;alert(1)&lt;/script&gt;'));
});
});
describe('renderBriefMagazine — envelope validation', () => {
it('throws when envelope is not an object', () => {
assert.throws(() => renderBriefMagazine(/** @type {any} */ (null)), /must be an object/);
assert.throws(() => renderBriefMagazine(/** @type {any} */ ('string')), /must be an object/);
});
it('throws when version is outside the supported set', () => {
const env = /** @type {any} */ ({ ...envelope(), version: 99 });
assert.throws(
() => renderBriefMagazine(env),
/is not in supported set/,
);
});
it('throws when issuedAt is missing or non-finite', () => {
const env = /** @type {any} */ ({ ...envelope() });
delete env.issuedAt;
assert.throws(() => renderBriefMagazine(env), /issuedAt/);
});
it('throws when envelope.data is missing', () => {
const env = /** @type {any} */ ({ version: BRIEF_ENVELOPE_VERSION, issuedAt: 0 });
assert.throws(() => renderBriefMagazine(env), /envelope\.data is required/);
});
it('throws when envelope.data.date is not YYYY-MM-DD', () => {
const env = envelope();
env.data.date = '04/17/2026';
assert.throws(() => renderBriefMagazine(env), /YYYY-MM-DD/);
});
it('throws when digest.signals is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.signals;
assert.throws(() => renderBriefMagazine(env), /digest\.signals must be an array/);
});
it('throws when digest.threads is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.threads;
assert.throws(() => renderBriefMagazine(env), /digest\.threads must be an array/);
});
it('throws when digest.numbers.clusters is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.numbers.clusters;
assert.throws(() => renderBriefMagazine(env), /digest\.numbers\.clusters/);
});
it('throws when a story has an invalid threatLevel', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).threatLevel = 'moderate';
assert.throws(
() => renderBriefMagazine(env),
/threatLevel must be one of critical\|high\|medium\|low/,
);
});
it('throws when stories is empty', () => {
const env = envelope({ stories: [] });
assert.throws(() => renderBriefMagazine(env), /stories must be a non-empty array/);
});
it('throws when a story carries an extension field (importanceScore, etc.)', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).importanceScore = 999;
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.stories\[0\] has unexpected key "importanceScore"/,
);
});
it('throws when envelope.data carries an extra key', () => {
const env = /** @type {any} */ (envelope());
env.data.primaryLink = 'https://leak.example/story';
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data has unexpected key "primaryLink"/,
);
});
it('throws when digest.numbers carries an extra key', () => {
const env = /** @type {any} */ (envelope());
env.data.digest.numbers.fetchedAt = Date.now();
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.digest\.numbers has unexpected key "fetchedAt"/,
);
});
it('throws when digest.numbers.surfaced does not equal stories.length', () => {
// Cover copy ("N threads that shaped the world today") and the
// at-a-glance stat both surface this count; the validator must
// keep them in lockstep so no brief can ship a self-contradictory
// number.
const env = envelope();
env.data.digest.numbers.surfaced = 99;
assert.throws(
() => renderBriefMagazine(env),
/surfaced=99 must equal.*stories\.length=4/,
);
});
});
describe('BRIEF_ENVELOPE_VERSION', () => {
it('is the literal 2 (bump requires cross-producer coordination)', () => {
assert.equal(BRIEF_ENVELOPE_VERSION, 2);
});
});
describe('renderBriefMagazine — v1 envelopes (back-compat window)', () => {
/**
* Build a v1-shaped envelope: version=1 and stories carry no
* sourceUrl. Emulates what's still resident in Redis under the 7-day
* TTL at the moment the v2 renderer deploys — the renderer must
* degrade gracefully instead of 404ing the still-live link.
*/
function v1Envelope() {
const v2 = envelope();
const stories = v2.data.stories.map(({ sourceUrl: _ignore, ...rest }) => rest);
return /** @type {any} */ ({ ...v2, version: 1, data: { ...v2.data, stories } });
}
it('accepts version=1 without sourceUrl and renders plain source line (no anchor)', () => {
const env = v1Envelope();
const html = renderBriefMagazine(env);
// No source-link anchors at all — v1 degrades to plain text.
assert.equal((html.match(/<a class="source-link"/g) ?? []).length, 0);
// The source label itself is still emitted for every story.
const labelCount = (html.match(/<div class="source">Source · /g) ?? []).length;
assert.equal(labelCount, env.data.stories.length);
});
it('still validates every v1 story field except sourceUrl', () => {
const env = v1Envelope();
env.data.stories[0].headline = '';
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.stories\[0\]\.headline must be a non-empty string/,
);
});
it('does not accept v1 with a malformed sourceUrl (defence-in-depth)', () => {
const env = v1Envelope();
env.data.stories[0].sourceUrl = 'javascript:alert(1)';
assert.throws(
() => renderBriefMagazine(env),
/sourceUrl .* is not allowed \(http\/https only\)/,
);
});
});
describe('renderBriefMagazine — source link (v2)', () => {
it('wraps every story source in an outgoing anchor with UTM params', () => {
const env = envelope();
const html = renderBriefMagazine(env);
// N stories → N source anchors.
const anchorCount = (html.match(/<a class="source-link"/g) ?? []).length;
assert.equal(anchorCount, env.data.stories.length);
// target=_blank + rel=noopener noreferrer are both present on each
// anchor. We match the literal attribute string the renderer emits.
assert.ok(html.includes('target="_blank" rel="noopener noreferrer"'));
// UTM params on every tracked href — check by presence of the
// four params at least once, plus the issueDate as utm_campaign.
assert.ok(html.includes('utm_source=worldmonitor'));
assert.ok(html.includes('utm_medium=brief'));
assert.ok(html.includes(`utm_campaign=${env.data.date}`));
assert.ok(html.includes('utm_content=story-01'));
assert.ok(html.includes('utm_content=story-02'));
});
it('escapes ampersands inside source URL query strings', () => {
// A real URL with multiple query params contains raw "&" characters
// that MUST be escaped to "&amp;" when interpolated into an href
// attribute — otherwise the HTML parser can terminate the href
// early on certain entity-like sequences (e.g. &copy=...).
const env = envelope({
stories: [
story({ sourceUrl: 'https://example.com/path?a=1&copy=2&b=3' }),
...envelope().data.stories.slice(1),
],
});
const html = renderBriefMagazine(env);
// The emitted href must contain escaped ampersands.
assert.ok(html.includes('?a=1&amp;copy=2&amp;b=3'), 'href ampersands must be escaped');
// Raw ampersand sequences (without the &amp;) must NOT appear in
// the emitted href for this story.
assert.ok(!/href="[^"]*?a=1&copy=/.test(html), 'raw ampersand leaked into href');
});
it('preserves pre-existing UTM tags on the upstream URL', () => {
const env = envelope({
stories: [
story({ sourceUrl: 'https://example.com/path?utm_source=publisher&utm_campaign=oem' }),
...envelope().data.stories.slice(1),
],
});
const html = renderBriefMagazine(env);
assert.ok(html.includes('utm_source=publisher'), 'publisher utm_source kept');
assert.ok(html.includes('utm_campaign=oem'), 'publisher utm_campaign kept');
// Ours still appended for the fields the publisher didn't set.
assert.ok(html.includes('utm_medium=brief'));
});
it('throws when sourceUrl is missing', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).sourceUrl = '';
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.stories\[0\]\.sourceUrl must be a non-empty string/,
);
});
it('throws when sourceUrl is not a parseable URL', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).sourceUrl = 'not a url';
assert.throws(
() => renderBriefMagazine(env),
/sourceUrl must be a parseable absolute URL/,
);
});
it('throws when sourceUrl uses a disallowed scheme', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).sourceUrl = 'javascript:alert(1)';
assert.throws(
() => renderBriefMagazine(env),
/sourceUrl .* is not allowed \(http\/https only\)/,
);
});
it('throws when sourceUrl carries userinfo credentials', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).sourceUrl = 'https://user:pass@example.com/x';
assert.throws(
() => renderBriefMagazine(env),
/sourceUrl must not include userinfo credentials/,
);
});
});
describe('renderBriefMagazine — Share button (non-public views)', () => {
const SHARE_URL = 'https://worldmonitor.app/api/brief/public/abcDEF012345';
it('renders a Share button with data-share-url and issue date', () => {
const env = envelope();
const html = renderBriefMagazine(env, { shareUrl: SHARE_URL });
assert.ok(html.includes('class="wm-share"'), 'share button must be present');
assert.ok(html.includes(`data-share-url="${SHARE_URL}"`), 'share button carries pre-derived URL');
assert.ok(html.includes(`data-issue-date="${env.data.date}"`), 'share button carries issue date');
assert.ok(html.includes('aria-label="Share this brief"'), 'share button has a11y label');
});
it('emits the inline share script exactly once', () => {
const env = envelope();
const html = renderBriefMagazine(env, { shareUrl: SHARE_URL });
const matches = html.match(/document\.querySelector\('\.wm-share'\)/g) ?? [];
assert.equal(matches.length, 1, 'share script emitted once');
});
it('click handler reads from dataset rather than fetching — no auth round-trip', () => {
const env = envelope();
const html = renderBriefMagazine(env, { shareUrl: SHARE_URL });
// The script must NOT contain any fetch call. It reads the URL
// from btn.dataset.shareUrl (server-derived) and invokes
// navigator.share / clipboard directly.
const scriptStart = html.indexOf("document.querySelector('.wm-share')");
const scriptEnd = html.indexOf('</script>', scriptStart);
assert.ok(scriptStart > -1 && scriptEnd > scriptStart);
const scriptBody = html.slice(scriptStart, scriptEnd);
assert.ok(!scriptBody.includes('fetch('), 'inline share script must not make fetch calls');
assert.ok(scriptBody.includes('btn.dataset.shareUrl'), 'script reads URL from dataset');
assert.ok(scriptBody.includes('navigator.share'), 'script uses Web Share API');
assert.ok(scriptBody.includes('navigator.clipboard'), 'script has clipboard fallback');
});
it('gracefully hides the Share button when shareUrl is absent', () => {
const env = envelope();
const html = renderBriefMagazine(env);
assert.ok(!html.includes('class="wm-share"'), 'no Share button when shareUrl unset');
assert.ok(
!html.includes("document.querySelector('.wm-share')"),
'no Share script when shareUrl unset',
);
});
it('HTML-escapes the shareUrl into the data attribute', () => {
const env = envelope();
const hostile = 'https://example.com/path?a=1&b="evil"';
const html = renderBriefMagazine(env, { shareUrl: hostile });
// The raw " must not appear inside data-share-url — would
// break the HTML attribute parser.
assert.ok(
html.includes('&quot;evil&quot;'),
'hostile quotes are HTML-escaped in data-share-url',
);
});
});
describe('renderBriefMagazine — publicMode', () => {
it('strips Share button + share script on public mode even when shareUrl is passed', () => {
const env = envelope();
const html = renderBriefMagazine(env, {
publicMode: true,
shareUrl: 'https://worldmonitor.app/api/brief/public/abcDEF012345',
});
assert.ok(!html.includes('class="wm-share"'), 'share button absent on public');
assert.ok(
!html.includes("document.querySelector('.wm-share')"),
'share script (runtime) absent on public',
);
});
it('replaces whyMatters with a generic callout on every story page', () => {
const env = envelope();
// Pre-test sanity: the fixture uses the Hormuz whyMatters on all
// stories, so we know it appears in the private render.
const privateHtml = renderBriefMagazine(env);
assert.ok(privateHtml.includes('Hormuz is roughly a fifth'), 'private render carries whyMatters');
const publicHtml = renderBriefMagazine(env, { publicMode: true });
assert.ok(!publicHtml.includes('Hormuz is roughly a fifth'), 'whyMatters stripped on public');
assert.ok(
publicHtml.includes('Subscribe to WorldMonitor Brief to see the full editorial'),
'generic placeholder callout rendered',
);
});
it('emits a noindex meta tag on public views', () => {
const env = envelope();
const privateHtml = renderBriefMagazine(env);
const publicHtml = renderBriefMagazine(env, { publicMode: true });
assert.ok(!privateHtml.includes('noindex'), 'private render is indexable');
assert.ok(publicHtml.includes('<meta name="robots" content="noindex,nofollow">'), 'public render sets noindex meta');
});
it('prepends a Subscribe strip on public views', () => {
const env = envelope();
const html = renderBriefMagazine(env, { publicMode: true });
assert.ok(html.includes('class="wm-public-strip"'), 'subscribe strip element emitted');
assert.ok(html.includes('worldmonitor.app/pro'), 'strip links to /pro');
assert.ok(html.includes('Subscribe'), 'strip CTA text present');
});
it('attaches ?ref= to public CTAs when refCode is provided', () => {
const env = envelope();
const html = renderBriefMagazine(env, { publicMode: true, refCode: 'ABC123' });
assert.ok(html.includes('worldmonitor.app/pro?ref=ABC123'), 'refCode appended to /pro URL');
});
it('HTML-escapes a hostile refCode before interpolation', () => {
const env = envelope();
const hostile = '"><script>1';
const html = renderBriefMagazine(env, { publicMode: true, refCode: hostile });
// encodeURIComponent handles most of the sanitisation; the result
// should contain percent-encoded chars and NO raw <script> tag.
assert.ok(!html.includes('<script>1'), 'raw hostile payload never appears');
});
it('swaps the back cover to a Subscribe CTA on public views', () => {
const env = envelope();
const privateHtml = renderBriefMagazine(env);
const publicHtml = renderBriefMagazine(env, { publicMode: true });
assert.ok(privateHtml.includes('End of'), 'private back cover reads "End of Transmission"');
assert.ok(publicHtml.includes('Get your own'), 'public back cover reads Subscribe-style headline');
assert.ok(publicHtml.includes('class="mono back-cta"'), 'public back cover has CTA anchor');
});
it('does NOT leak the original user name on public views', () => {
const env = envelope({
user: { name: 'Alice Personally', tz: 'UTC' },
});
const html = renderBriefMagazine(env, { publicMode: true });
assert.ok(!html.includes('Alice Personally'), 'user name must not appear on public mirror');
});
it('keeps story headlines, categories, sources on public views (shared content)', () => {
const env = envelope();
const html = renderBriefMagazine(env, { publicMode: true });
// Story content itself IS shared — that's the point of the mirror.
assert.ok(html.includes('Iran declares Strait of Hormuz open'));
assert.ok(html.includes('Multiple wires'));
});
it('default options (no second arg) behaves identically to the private path', () => {
const env = envelope();
const a = renderBriefMagazine(env);
const b = renderBriefMagazine(env, {});
assert.equal(a, b);
});
});
// ── Regression: cover greeting follows envelope.data.digest.greeting ─────────
// Previously the cover hardcoded "Good evening" regardless of issue time, so
// a brief composed at 13:02 local (envelope greeting = "Good afternoon.")
// rendered "Good evening" on the cover and "Good afternoon." on slide 2 —
// visibly inconsistent. Fix wires digest.greeting into the cover (period
// stripped for the mono-cased slot).
describe('cover greeting ↔ digest.greeting parity', () => {
/**
* Extract the cover <section> so we can assert on it in isolation without
* matching the identical greeting that appears on slide 2.
*/
function extractCover(html) {
const match = html.match(/<section class="page cover">[\s\S]*?<\/section>/);
assert.ok(match, 'cover section must be present');
return match[0];
}
it('renders "Good afternoon" on the cover when digest.greeting is "Good afternoon."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good afternoon.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good afternoon<'), `cover should contain "Good afternoon" without period, got: ${cover}`);
assert.ok(!cover.includes('Good evening'), 'cover must NOT say "Good evening" when digest.greeting is afternoon');
});
it('renders "Good morning" on the cover when digest.greeting is "Good morning."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good morning.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good morning<'));
assert.ok(!cover.includes('Good evening'));
assert.ok(!cover.includes('Good afternoon'));
});
it('renders "Good evening" on the cover when digest.greeting is "Good evening."', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good evening.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(cover.includes('>Good evening<'));
});
it('strips trailing period(s) — cover is mono-cased, no punctuation', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: 'Good afternoon...' },
});
const cover = extractCover(renderBriefMagazine(env));
// Envelope can send any trailing dot count; cover strips all of them.
assert.ok(cover.includes('>Good afternoon<'));
assert.ok(!cover.includes('Good afternoon.'));
});
it('HTML-escapes the greeting (defense-in-depth, even though envelope values are controlled)', () => {
const env = envelope({
digest: { ...envelope().data.digest, greeting: '<script>alert(1)</script>.' },
});
const cover = extractCover(renderBriefMagazine(env));
assert.ok(!cover.includes('<script>alert'));
assert.ok(cover.includes('&lt;script&gt;'));
});
});