Files
memind/wechat-daily-news-inline.test.mjs

278 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
convertDailyNewsHtmlToWechatInlineArticle,
wechatDailyNewsInlineInternals,
} from './wechat-daily-news-inline.mjs';
const WEATHER_HTML = `<!DOCTYPE html><html><body><div class="container">
<div class="hero"><h1>📰 每日新闻早报</h1></div>
<div class="quick-links"><a href="#weather">🌤️ 天气</a><a href="#world">🌍 国际</a></div>
<div class="section" id="headlines"><div class="section-title">要闻</div><div class="card"><h3>头条</h3><p>正文</p></div></div>
<div class="section" id="weather">
<div class="section-title">🌤️ 今日天气</div>
<div class="weather-today">
<div class="wt-row"><div><div class="wt-city">📍 北京</div><div class="wt-detail">☀️ 晴 · 温差大</div></div><div><div class="wt-temp-big">30°</div></div></div>
<div style="display:flex;gap:12px;"><span>🌧️ 华南有雨</span><span>☀️ 华北晴好</span></div>
<div style="background:rgba(255,255,255,0.5);padding:8px;"><strong>中央气象台:</strong>未来三天降水减弱。</div>
</div>
<div class="weather-grid">
<div class="weather-card w-hot"><div class="w-left"><span class="w-city">北京</span><span class="w-cond">☀️ 晴</span></div><div class="w-right"><span class="w-temp">30°</span></div></div>
<div class="weather-card w-rain"><div class="w-left"><span class="w-city">上海</span><span class="w-cond">🌧️ 小雨</span></div><div class="w-right"><span class="w-temp">26°</span></div></div>
</div>
<p style="font-size:12px;color:#a0aec0;">数据来源:中央气象台</p>
</div>
</div></body></html>`;
test('splitSections preserves section ids after opening tag split', () => {
const sections = wechatDailyNewsInlineInternals.splitSections(WEATHER_HTML);
assert.deepEqual(sections.map((item) => item.id), ['headlines', 'weather']);
});
test('renderWeatherSection uses tourist city title and two-column city cards', () => {
const sections = wechatDailyNewsInlineInternals.splitSections(WEATHER_HTML);
const weather = sections.find((item) => item.id === 'weather');
const rendered = wechatDailyNewsInlineInternals.renderWeatherSection(weather.html);
assert.match(rendered, /热门旅游城市天气/u);
assert.match(rendered, /📍 北京/u);
assert.match(rendered, /☀️ 晴 · 温差大/u);
assert.match(rendered, /width:50%;box-sizing:border-box/u);
assert.match(rendered, /display:inline-block/u);
assert.doesNotMatch(rendered, /display:flex/u);
assert.match(rendered, /30°/u);
assert.match(rendered, /26°/u);
assert.doesNotMatch(rendered, /中央气象台/u);
assert.doesNotMatch(rendered, /今日天气/u);
});
test('renderWeatherSection skips regional fallback cards and national summary', () => {
const regionalWeatherHtml = `<div class="section" id="weather">
<div class="section-title">🌤️ 今日天气 · 9月16日</div>
<div class="weather-today">
<div class="wt-row"><div><div class="wt-city">📍 全国概况 · 江汉西南阴雨开场</div><div class="wt-detail">🌧️ 湖北西部局地暴雨</div></div></div>
</div>
<div class="weather-grid">
<div class="weather-card"><div class="w-left"><span class="w-city">湖北西部</span><span class="w-cond">🌧️ 暴雨</span></div><div class="w-right"><span class="w-temp">雨</span></div></div>
<div class="weather-card"><div class="w-left"><span class="w-city">北京</span><span class="w-cond">☀️ 晴</span></div><div class="w-right"><span class="w-temp">30°</span></div></div>
<div class="weather-card"><div class="w-left"><span class="w-city">上海</span><span class="w-cond">🌧️ 小雨</span></div><div class="w-right"><span class="w-temp">26°</span></div></div>
</div>
</div>`;
const rendered = wechatDailyNewsInlineInternals.renderWeatherSection(regionalWeatherHtml);
assert.match(rendered, /热门旅游城市天气 · 9月16日/u);
assert.match(rendered, /北京/u);
assert.match(rendered, /上海/u);
assert.doesNotMatch(rendered, /湖北西部/u);
assert.doesNotMatch(rendered, /全国概况/u);
});
test('renderWeatherSection supports news002 wc/we/wt cards', () => {
const news002Weather = `<div class="section" id="weather">
<div class="section-title">🌤️ 今日天气 · 9月24日</div>
<div class="weather-today"><div><div class="c">全国重点城市 · 9月24日</div><div class="d">中央气象台预报,广东广西局地大到暴雨</div></div></div>
<div class="weather-grid">
<div class="weather-card"><div class="wc">北京</div><div class="we">🌤</div><div class="wt">14/26℃</div></div>
<div class="weather-card"><div class="wc">上海</div><div class="we">🌧</div><div class="wt">22/28℃</div></div>
</div>
</div>`;
const rendered = wechatDailyNewsInlineInternals.renderWeatherSection(news002Weather);
assert.match(rendered, /北京/u);
assert.match(rendered, /14\/26℃/u);
assert.match(rendered, /上海/u);
assert.match(rendered, /22\/28℃/u);
});
test('buildWechatInlineContent renders weather and optional quick links', () => {
const built = wechatDailyNewsInlineInternals.buildWechatInlineContent(WEATHER_HTML, {
includeFixedLayout: false,
trimProfile: { skipHistory: true, skipQuickLinks: false },
});
assert.match(built.content, /🌤️ 天气/u);
assert.match(built.content, /📍 北京/u);
assert.match(built.content, /30°/u);
});
const HISTORY_HTML = `<!DOCTYPE html><html><body><div class="container">
<div class="hero"><h1>📰 每日新闻早报</h1></div>
<div class="section" id="headlines"><div class="section-title">要闻</div><div class="card"><h3>头条</h3><p>正文</p></div></div>
<div class="section" id="history">
<div class="section-title">📜 历史上的今天 · 9月24日</div>
<ol class="history-list">
<li class="history-item"><span class="hy">1949年</span><span class="he">中国人民政治协商会议第一届全体会议开幕。</span></li>
<li class="history-item"><span class="hy">1957年</span><span class="he">美国小石城中央高中发生种族融合冲突。</span></li>
<li class="history-item"><span class="hy">1996年</span><span class="he">《全面禁止核试验条约》在联合国开放签署。</span></li>
<li class="history-item"><span class="hy">2008年</span><span class="he">神舟七号载人飞船成功发射。</span></li>
</ol>
</div>
</div></body></html>`;
test('renderHistorySection renders up to three compact history items', () => {
const sections = wechatDailyNewsInlineInternals.splitSections(HISTORY_HTML);
const history = sections.find((item) => item.id === 'history');
const rendered = wechatDailyNewsInlineInternals.renderHistorySection(history.html);
assert.match(rendered, /历史上的今天 · 9月24日/u);
assert.match(rendered, /1949年/u);
assert.match(rendered, /1957年/u);
assert.match(rendered, /1996年/u);
assert.doesNotMatch(rendered, /2008年/u);
assert.match(rendered, /中国人民政治协商会议/u);
});
test('buildWechatInlineContent includes history snippet by default', () => {
const built = wechatDailyNewsInlineInternals.buildWechatInlineContent(HISTORY_HTML, {
includeFixedLayout: false,
});
assert.match(built.content, /历史上的今天 · 9月24日/u);
assert.match(built.content, /1949年/u);
assert.doesNotMatch(built.content, /2008年/u);
});
test('buildWechatInlineContent can skip history when trimProfile requests it', () => {
const built = wechatDailyNewsInlineInternals.buildWechatInlineContent(HISTORY_HTML, {
includeFixedLayout: false,
trimProfile: { skipHistory: true },
});
assert.doesNotMatch(built.content, /历史上的今天/u);
});
test('convertDailyNewsHtmlToWechatInlineArticle keeps weather when trimming', () => {
const article = convertDailyNewsHtmlToWechatInlineArticle(WEATHER_HTML, {
includeFixedLayout: false,
fixedLayoutLength: 1200,
});
assert.match(article.content, /📍 北京/u);
assert.match(article.content, /30°/u);
});
test('trimWechatInlineBodyByRemovingCards drops tail cards instead of hard slicing', () => {
const html = `${WEATHER_HTML.slice(0, -24)}<div class="section" id="world"><div class="section-title">国际</div><div class="card"><h3>国际一</h3><p>正文一</p></div><div class="card"><h3>国际二</h3><p>正文二</p></div></div></div></body></html>`;
const built = wechatDailyNewsInlineInternals.buildWechatInlineContent(html, {
includeFixedLayout: false,
});
const beforeCards = wechatDailyNewsInlineInternals.countInlineCards(built.content);
const trimmed = wechatDailyNewsInlineInternals.trimWechatInlineBodyByRemovingCards(
built.content,
built.content.length - 260,
{ minCards: 1 },
);
const afterCards = wechatDailyNewsInlineInternals.countInlineCards(trimmed);
assert.ok(beforeCards >= 3);
assert.ok(afterCards < beforeCards);
assert.ok(trimmed.length < built.content.length);
assert.match(trimmed, /<\/section>\s*$/u);
});
test('section list numbering starts at 01 after feature cards', () => {
const html = `<!doctype html><html><body>
<div class="hero"><h1>每日新闻早报</h1><div class="subtitle">头条一 · 头条二</div></div>
<div class="section" id="headlines">
<div class="section-title">今日要闻</div>
<div class="card highlight-box"><h3>头条卡</h3><p>重点摘要</p></div>
<div class="card"><h3>普通要闻</h3><p>普通摘要</p></div>
</div>
<div class="section" id="world">
<div class="section-title">国际热点</div>
<div class="card"><h3>国际一</h3><p>国际摘要一</p></div>
</div>
</body></html>`;
const built = wechatDailyNewsInlineInternals.buildWechatInlineContent(html, {
includeFixedLayout: false,
});
assert.match(built.content, /头条卡[\s\S]*?01<\/span> <strong>普通要闻/u);
assert.match(built.content, /WORLD[\s\S]*?01<\/span> <strong>国际一/u);
assert.doesNotMatch(built.content, /02<\/span> <strong>普通要闻/u);
});
test('hero highlights render as a separate readable digest', () => {
const digest = wechatDailyNewsInlineInternals.renderTodayDigest(
'<div class="subtitle">平陆运河通航 · 海上火箭发射 · 全球市场聚焦议息</div>',
);
assert.match(digest, /今日导读/u);
assert.match(digest, /平陆运河通航<br>/u);
assert.match(digest, /海上火箭发射<br>/u);
});
test('renderHero does not insert the cover image as a large logo', () => {
const html = `<div class="hero"><h1>每日新闻早报</h1><div class="date-badge">9月16日</div></div>
<meta name="mindspace-cover" content='{"cover":"https://mmbiz.qpic.cn/logo.png"}'>`;
const rendered = wechatDailyNewsInlineInternals.renderHero(html, new Map([
['https://mmbiz.qpic.cn/logo.png', 'https://mmbiz.qpic.cn/logo.png'],
]));
assert.match(rendered, /每日新闻早报/u);
assert.doesNotMatch(rendered, /<img\b/u);
assert.doesNotMatch(rendered, /TKMIND · DAILY BRIEF/u);
});
const NEWS002_HTML = `<!DOCTYPE html><html><body>
<div class="masthead"><div class="kicker">TKMIND DAILY</div><h1>每日新闻早报</h1>
<div class="meta"><b>2026年9月21日</b><span>星期一</span><span>第 221 期</span></div></div>
<div class="lede">中美外长再次通话 · 东博会南宁开幕 · Kimi 发布金融方案</div>
<div class="container">
<div class="section" id="headlines"><div class="section-title"><h2>今日头条</h2></div>
<a class="lead-card" href="https://example.com/a" data-event-key="k1"><div class="media ph">📰</div>
<div class="body"><span class="tag">中美关系</span><h3>王毅同鲁比奥通电话</h3><p>头条正文摘要。</p>
<div class="source"><a href="https://example.com/a">新华社</a></div></div></a>
</div>
<div class="section" id="domestic"><div class="section-title"><h2>国内</h2></div>
<div class="card" data-event-key="k2"><div class="media ph">🇨🇳</div>
<div class="body"><span class="tag">时政</span><h3>沈阳举行九一八纪念活动</h3><p>国内正文摘要。</p>
<div class="why"><b>为什么重要</b>:影响当天交通管制。</div>
<div class="source"><a href="https://example.com/b">人民日报</a></div></div></div>
</div>
</div></body></html>`;
test('news002 lead-card renders as compact left-thumb card in the wechat draft', () => {
const rendered = wechatDailyNewsInlineInternals.renderGenericSection(
NEWS002_HTML.match(/<div class="section" id="headlines">[\s\S]*?<\/a>\n<\/div>/)[0],
{ sectionId: 'headlines', news002Mode: true },
);
assert.match(rendered, /王毅同鲁比奥通电话/u);
assert.match(rendered, /中美关系/u, 'lead-card 的 tag 必须保留');
assert.doesNotMatch(rendered, /01<\/span>/u, '头条卡不占列表序号');
assert.doesNotMatch(rendered, /calc\(100% \+ 28px\)/u, 'news002 头条不走全宽 feature 大图');
});
test('news002 card with media block still yields title and body', () => {
const rendered = wechatDailyNewsInlineInternals.renderGenericSection(
NEWS002_HTML.match(/<div class="section" id="domestic">[\s\S]*$/)[0],
{ sectionId: 'domestic', news002Mode: true },
);
assert.match(rendered, /沈阳举行九一八纪念活动/u);
assert.match(rendered, /国内正文摘要/u, '.media 在前不应挤掉正文 <p>');
});
test('news002 cards render uploaded source images in the wechat draft', () => {
const sourceImage = 'https://news.example/story.jpg';
const wechatImage = 'https://mmbiz.qpic.cn/wechat-story.jpg';
const html = NEWS002_HTML
.replace('<div class="media ph">📰</div>', `<div class="media"><img src="${sourceImage}" alt="头条"></div>`)
.replace('<div class="media ph">🇨🇳</div>', `<div class="media"><img src="${sourceImage}" alt="国内"></div>`);
const imageUrlMap = new Map([[sourceImage, wechatImage]]);
const lead = wechatDailyNewsInlineInternals.renderGenericSection(
html.match(/<div class="section" id="headlines">[\s\S]*?<\/a>\n<\/div>/)[0],
{ sectionId: 'headlines', imageUrlMap, news002Mode: true },
);
const domestic = wechatDailyNewsInlineInternals.renderGenericSection(
html.match(/<div class="section" id="domestic">[\s\S]*$/)[0],
{ sectionId: 'domestic', imageUrlMap, news002Mode: true },
);
assert.match(lead, /mmbiz\.qpic\.cn\/wechat-story\.jpg/u);
assert.match(domestic, /mmbiz\.qpic\.cn\/wechat-story\.jpg/u);
assert.doesNotMatch(lead, /news\.example\/story\.jpg/u);
assert.match(lead, /width:72px;height:54px/u, '配图必须是左上角小图');
assert.doesNotMatch(domestic, /width:100%;height:auto;margin:0 0 9px/u, '列表卡不再用顶部全宽图');
});
test('renderHero reads news002 masthead meta and lede', () => {
const rendered = wechatDailyNewsInlineInternals.renderHero(NEWS002_HTML);
assert.match(rendered, /每日新闻早报/u);
assert.match(rendered, /2026年9月21日/u, 'news002 用 .meta 取代 .date-badge');
assert.match(rendered, /东博会南宁开幕/u, 'news002 用 .lede 取代 .subtitle');
});
test('renderTodayDigest works off the news002 lede', () => {
const digest = wechatDailyNewsInlineInternals.renderTodayDigest(NEWS002_HTML);
assert.match(digest, /今日导读/u);
assert.match(digest, /中美外长再次通话<br>/u);
});