fix: recover morning news source images and preserve localized photos
Memind CI / Test, build, and release guards (push) Successful in 5m40s
Memind CI / Test, build, and release guards (push) Successful in 5m40s
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
# 历史分支处置登记
|
||||
|
||||
## `feature/news-morning-staging-title-digest`
|
||||
|
||||
**状态:未合并/只读封存/禁止再次引用。不是待开发分支,不阻塞新的开发分支。**
|
||||
|
||||
审计日期:2026-10-19。用户已明确批准保留旧分支和提交供只读追溯、不合并其功能,从干净主线新建图片修复分支,登记随本次提交。
|
||||
|
||||
- 保留提交:`698c6a64a7d9c40f5e7a75fda350f187b5b41262`(2026-09-23)。
|
||||
- 该提交不属于当前 `origin/main`,包含早报安全阶段暂存、微信标题摘要等旧提案。本次不合并、不 cherry-pick、不发布这些功能。
|
||||
- 该历史分支和提交仅可只读追溯;不得再作为开发基线、合并来源、cherry-pick 来源或发布来源。
|
||||
- 本次图片修复基于当前完整主线 `05b440ce6af460a4b8eb03f48bc2671846d243df`,与旧分支无代码继承关系。
|
||||
|
||||
## `fix/morning-greeting-daily-continue`
|
||||
|
||||
**状态:已提交 / 已进入 main / 禁止再次引用。该分支保留仅用于只读追溯,不是待合并开发分支;未创建远端功能分支。**
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# 2026-10-18 早报没有真实封面与新闻配图
|
||||
|
||||
## 生产证据
|
||||
|
||||
- 当天 03:36:35 已自动进入微信草稿,草稿 ID 为 `GR19aTT35YYg2GKlTdWRZbGvkmgw3vpRdRUAF-42Z1WFdwoQWjuHW4EjS8s9q3mq`。
|
||||
- 页面实际含 29 张新闻卡片,但 `<img>` 数量为 0;worker 记录 attempted=29、resolved=0、imageCount=0。
|
||||
- 本地 `.thumbnail.svg` 为系统生成的通用渐变/山形兜底封面。微信 `thumb_media_id` 和 CDN 图片 HTTP 200 只能说明存在素材,不能说明是实际新闻照片。
|
||||
- 29 个来源页面逐一检查:15 篇在中新网 `.left_zw` 正文中含真实新闻照片;它们没有 `og:image` 等元数据。
|
||||
- 对同一真实图片实测:带 TKMind-NewsBot 标记的 User-Agent 得到 403;标准浏览器 User-Agent 得到 200。仅补正文提取仍会被可用性探测剔除。
|
||||
|
||||
## 本地修复
|
||||
|
||||
- `wechat-news-morning-images.mjs`:元数据没有可用图片时,在明确的文章正文容器提取照片,支持协议相对链接与懒加载属性;不从站点页头、推荐新闻、脚本或注释取图。
|
||||
- 使用标准浏览器 User-Agent;图片探测携带原文 Referer,HEAD 403/405/501 时改用 GET,并释放响应流。
|
||||
- 重复 enrichment 保留已本地化的真实图片,避免将本地文件当远程 URL 探测失败而删除。
|
||||
- 保留现有有图则展示、无真实图则保留文字卡的业务策略,不加入默认新闻小图,也不阻断出稿。
|
||||
- 修正两处与当前模板/开发环境不一致的既有测试:卡片超限测试显式设置上限;默认缩略图复制测试自行创建 fixture,不依赖工作区里是否存在默认资产。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 图片、worker、草稿相关单测 42/42 通过。
|
||||
- 使用生产原稿(SHA256 `08a06f9f32bf762d7ec5d75753d510f324e16aae372bf43fdf9743f9f07bd34a`)验证:找回 15 张真实配图,15 张全部下载本地,失败 0。
|
||||
- 修复前后 29 张卡片的标题、来源链接与 event key 全部一致。
|
||||
- 真实微信正文渲染器预演保留 9 张新闻图,正文长度 19,299 字符;其余裁剪受既有微信 20,000 字符规则影响,未更改裁剪策略。
|
||||
- 本地预演目录:`/tmp/memind-news-1018-repair/`,包含修复页面、15 张本地配图、从正文真实照片裁切的封面、微信正文预演及 `repair-plan.json`。
|
||||
- 封面已实际打开核对为对应「中韩联合巡航」新闻的海警船照片,非兜底渐变图。浏览器截图验证受本机浏览器缺失/沙箱启动失败限制,未将微信渲染器字符串验证描述为浏览器视觉验收。
|
||||
- 本次尚未写入生产页面或修改微信原草稿,未发布代码。更新当天内容及原草稿应在上述验证后取得明确批准,并先备份旧页面/封面/草稿,核对原稿 hash,使用 draft/update 更新原草稿以避免重复。
|
||||
|
||||
## 待完成
|
||||
|
||||
1. 批准后把已核对的当天图片和页面作为内容修复写入生产,替换兜底封面;不上传本机业务源码。
|
||||
2. 使用正式微信图片素材接口上传真实图片,更新同一草稿的正文和 thumb_media_id;读回验证新闻图数量、素材可访问性和封面实际内容。
|
||||
3. 本地代码变更仍需按分支闭环与整包发布规则进入主线后上线;当前脏 main 不能直接发布。
|
||||
|
||||
## 已获批准的当天内容修复
|
||||
|
||||
用户通过确认项明确批准「更新今天线上早报和原微信草稿」。已使用 `scripts/apply-news-morning-image-repair.mjs` 执行,未上传运行时代码、未创建新草稿。
|
||||
|
||||
- 原页面、封面、原微信正文及 17 个内容文件写入清单已备份到生产 `data/news-image-repair-1792272917656/`。完整验收收据为该目录的 `receipt.json`。
|
||||
- 写入前核对原页面 SHA256,以及草稿标题、正文、封面等可编辑字段,防止覆盖期间的修改;微信临时预览 URL 每次变化,不能用整个 draft/get JSON 作为并发指纹。
|
||||
- 页面写入 15 张真实新闻图,并在 mindspace-cover 元数据中显式绑定真实照片,避免后续再次生成通用兜底封面。页面新 SHA256:`6d7f2e6547ab676ef4b748379aed08d3dbf88c4cca3a84c9885eefb5cdbd4185`。
|
||||
- 公网页面读回 15 个 `<img>`,15 个图片链接全部 HTTP 200。
|
||||
- 15 张新闻图全部上传微信;使用真实 CDN URL 和原稿二维码重新渲染后,最终微信正文按现有 20,000 字符限制保留 7 张新闻配图,加原二维码共 8 图,提交正文 19,392 字符。
|
||||
- 更新的是同一份 `GR19aTT35YYg2GKlTdWRZbGvkmgw3vpRdRUAF-42Z1WFdwoQWjuHW4EjS8s9q3mq` 草稿,没有重复创建。
|
||||
- 微信会将图片改为 data-src、调整 CDN 尺寸及移除不支持的 CSS,所以不能用 HTML 字符串逐字相等验收。已核对完整正文文字、按顺序排列的图片、标题、作者、摘要、阅读原文地址和评论设置均匹配。
|
||||
- 封面被微信异步转为裁切后的 thumb_url,此时 thumb_media_id 为空,但真实封面 URL 可读。已下载解码封面进行内容核对:540×229 JPEG,与所提交新闻照片按相同比例中心裁切后的像素均方误差为 61.31;验证的是照片实际内容,不是单纯 HTTP 200。
|
||||
- 7 张微信新闻图全部 HTTP 200。47 项图片、worker、草稿及写入保护相关测试全部通过。
|
||||
|
||||
当天页面与草稿修复已完成。提取器修复和辅助脚本仍为本地改动,尚未提交或发布;后续长期防复发发布仍需按仓库分支及整包发布规则处理。
|
||||
|
||||
续查中还发现 05:29:06 的 auto-push 记录指向另一份 media_id,该 ID 在微信 draft/get 返回 40007;未据此创建第三份草稿或改动其记录。本次明确验收的是上述仍可读取的原草稿。
|
||||
|
||||
## 防复发发布准备与阻断
|
||||
|
||||
用户同意推进防复发修复发布后,已完成:
|
||||
|
||||
- 图片、草稿、worker、配置、检索、模板、去重、内容安全等相关回归 110 项全部通过。
|
||||
- 为排除对其他未提交工作的依赖,单独取 main HEAD 的三个纯函数依赖文件,在普通临时测试目录中验证图片模块与测试,20/20 通过;没有创建 Git 分支或 worktree。
|
||||
- 两文件补丁与验证记录位于 `.local/news-morning-image-release/`。补丁只包含 `wechat-news-morning-images.mjs` 与其测试,也保留了这两个文件中此前已有的图片质量告警修改;不包含学习页、发布脚本或检索/RSS 的其他改动。
|
||||
- 执行 `ALLOW_MAIN_RELEASE=1 bash scripts/check-release-ready.sh --skip-fetch`,明确被 `worktree has uncommitted or untracked changes` 阻断。核对时工作区共 121 项未提交/未跟踪路径;未清理、暂存或丢弃其他任务的改动。
|
||||
- 当时会话权限将 `.git` 设为只读,无法合法完成提交、分支与主线闭环。没有尝试绕过该权限,也没有构建 runtime、推送、合并或发布。
|
||||
|
||||
发布授权已经收到;剩余阻断是工作区整理与 Git 写权限,并非再次索要发布批准。解除后须按现有规则继续提交、同步主线、CI、整包与生产 Gate,而不是单文件上传线上源码。当前不能保证次日自动生成已经使用修复后的提取器。
|
||||
|
||||
后续用户明确允许 Git 写入,正常 `git add` 已成功。图片代码与测试将单独提交;其余未提交内容需按 portal-release 流程先取得临时 stash 的单独同意,发布后恢复。发布前基础数据库、能力、provider 与微信测试已通过。
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
const DEFAULT_TIMEOUT_MS = 6000;
|
||||
const DEFAULT_USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
||||
+ '(KHTML, like Gecko) Chrome/126.0 Safari/537.36 TKMind-NewsBot/1.0';
|
||||
+ '(KHTML, like Gecko) Chrome/126.0 Safari/537.36';
|
||||
const THIS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const NEWS002_CARD_FALLBACK_FILENAME = 'news002-card-fallback.svg';
|
||||
const NEWS002_LOCAL_IMAGE_MAX_BYTES = 2.5 * 1024 * 1024;
|
||||
@@ -178,7 +178,30 @@ export function extractNewsMorningSourceImage(html, pageUrl = '') {
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
const normalized = normalizeNewsMorningImageUrl(candidate, { pageUrl });
|
||||
if (normalized) return normalized;
|
||||
if (normalized && !isWeakNewsMorningImageUrl(normalized)) return normalized;
|
||||
}
|
||||
// Chinanews articles frequently have no image metadata. Read only an explicit
|
||||
// article body, never the first image on the whole page (logo/related stories).
|
||||
const source = String(html ?? '').replace(/<!--[\s\S]*?-->|<script\b[^>]*>[\s\S]*?<\/script\s*>|<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, '');
|
||||
const bodyOpen = /<(div|section|article)\b[^>]*>/gi;
|
||||
let match;
|
||||
while ((match = bodyOpen.exec(source))) {
|
||||
const classes = attrOf(match[0], 'class').split(/\s+/);
|
||||
const isBody = attrOf(match[0], 'itemprop').split(/\s+/).includes('articleBody')
|
||||
|| classes.some((name) => ['left_zw', 'article-body', 'article-content', 'entry-content'].includes(name));
|
||||
if (!isBody) continue;
|
||||
const { end } = sliceBalancedBlock(source, match[1], match.index, bodyOpen.lastIndex);
|
||||
const body = source.slice(bodyOpen.lastIndex, end);
|
||||
for (const image of body.match(/<img\b[^>]*>/gi) ?? []) {
|
||||
// Ignore tracking pixels and explicit decorative images inside the body.
|
||||
if (/^(?:presentation|none)$/i.test(attrOf(image, 'role')) || attrOf(image, 'aria-hidden') === 'true') continue;
|
||||
if (['width', 'height'].some((key) => /^\d+$/.test(attrOf(image, key)) && Number(attrOf(image, key)) < 64)) continue;
|
||||
for (const key of ['data-original', 'data-src', 'src']) {
|
||||
const normalized = normalizeNewsMorningImageUrl(attrOf(image, key), { pageUrl });
|
||||
if (normalized && !isWeakNewsMorningImageUrl(normalized)) return normalized;
|
||||
}
|
||||
}
|
||||
bodyOpen.lastIndex = end;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -189,6 +212,7 @@ export async function isFetchableNewsMorningImageUrl(
|
||||
fetchImpl = fetch,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
userAgent = DEFAULT_USER_AGENT,
|
||||
referrer = '',
|
||||
} = {},
|
||||
) {
|
||||
const normalized = normalizeNewsMorningImageUrl(imageUrl);
|
||||
@@ -200,16 +224,19 @@ export async function isFetchableNewsMorningImageUrl(
|
||||
method: 'HEAD',
|
||||
redirect: 'follow',
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8' },
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8', ...(referrer ? { Referer: referrer } : {}) },
|
||||
});
|
||||
if (response.status === 405 || response.status === 501) {
|
||||
if ([403, 405, 501].includes(response.status)) {
|
||||
await response.body?.cancel?.();
|
||||
response = await fetchImpl(normalized, {
|
||||
redirect: 'follow',
|
||||
signal: controller.signal,
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8' },
|
||||
headers: { 'User-Agent': userAgent, Accept: 'image/*,*/*;q=0.8', ...(referrer ? { Referer: referrer } : {}) },
|
||||
});
|
||||
}
|
||||
return response.ok;
|
||||
const ok = response.ok;
|
||||
await response.body?.cancel?.();
|
||||
return ok;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
@@ -690,10 +717,11 @@ export async function enrichNews002HtmlWithSourceImages(
|
||||
|
||||
async function resolveCardImage(card) {
|
||||
const existingImage = extractCardImage(card.raw);
|
||||
if (normalizeNewsMorningLocalImageRef(existingImage)) return existingImage;
|
||||
if (
|
||||
existingImage
|
||||
&& !isWeakNewsMorningImageUrl(existingImage)
|
||||
&& await isFetchableNewsMorningImageUrl(existingImage, { fetchImpl, timeoutMs })
|
||||
&& await isFetchableNewsMorningImageUrl(existingImage, { fetchImpl, timeoutMs, referrer: card.url })
|
||||
) {
|
||||
return existingImage;
|
||||
}
|
||||
@@ -703,7 +731,7 @@ export async function enrichNews002HtmlWithSourceImages(
|
||||
if (
|
||||
fromSource
|
||||
&& !isWeakNewsMorningImageUrl(fromSource)
|
||||
&& await isFetchableNewsMorningImageUrl(fromSource, { fetchImpl, timeoutMs })
|
||||
&& await isFetchableNewsMorningImageUrl(fromSource, { fetchImpl, timeoutMs, referrer: card.url })
|
||||
) {
|
||||
return fromSource;
|
||||
}
|
||||
@@ -839,9 +867,24 @@ export function auditNews002ImageCoverage(
|
||||
sectionFill = auditNews002SectionFill(source, { minFlexSections, exemptSectionIds });
|
||||
violations.push(...sectionFill.violations);
|
||||
}
|
||||
// Quality warnings are informational: never turn missing images/sources into
|
||||
// a publication failure or a new automatic regeneration loop.
|
||||
const nonHomepageSourceCount = cards.filter((card) => {
|
||||
try {
|
||||
const url = new URL(card.url);
|
||||
return /^https?:$/.test(url.protocol) && !/^\/(?:index\.[^/]+)?$/i.test(url.pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}).length;
|
||||
const qualityWarnings = [];
|
||||
if (cards.length && missingImages.length === cards.length) qualityWarnings.push('no-story-images');
|
||||
if (cards.length && !nonHomepageSourceCount) qualityWarnings.push('no-nonhomepage-source-links');
|
||||
return {
|
||||
ok: violations.length === 0,
|
||||
violations,
|
||||
qualityWarnings,
|
||||
nonHomepageSourceCount,
|
||||
cardCount: cards.length,
|
||||
imageCount: cards.length - missingImages.length,
|
||||
leadCount: headlineLeadCount,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
auditNews002ImageCoverage,
|
||||
cardHasDisplayableImage,
|
||||
isWeakNewsMorningImageUrl,
|
||||
isFetchableNewsMorningImageUrl,
|
||||
enrichNews002HtmlWithSourceImages,
|
||||
enrichNewsMorningSearchGroupsWithImages,
|
||||
extractNewsMorningSourceImage,
|
||||
@@ -57,6 +58,57 @@ test('enrichNewsMorningSearchGroupsWithImages reads source article metadata', as
|
||||
assert.equal(result.groups[0].results[0].imageSource, 'article-meta');
|
||||
});
|
||||
|
||||
test('source image fallback reads Chinanews body photos, excluding logo and related news', () => {
|
||||
const page = 'https://www.chinanews.com.cn/gn/2026/10-17/10716271.shtml';
|
||||
const html = `<img src="https://cdn.example/site-logo.jpg">
|
||||
<div class="left_zw"><p>中韩联合巡航</p><div><img src="//i2.chinanews.com.cn/simg/patrol.jpg"></div></div>
|
||||
<div class="related"><img src="https://cdn.example/unrelated.jpg"></div>`;
|
||||
assert.equal(extractNewsMorningSourceImage(html, page), 'https://i2.chinanews.com.cn/simg/patrol.jpg');
|
||||
assert.equal(extractNewsMorningSourceImage(html.replace(/<img src="\/\/i2[^>]*>/, ''), page), '');
|
||||
});
|
||||
|
||||
test('source image fallback handles lazy body images and skips tracking/default assets', () => {
|
||||
const html = `<meta property="og:image" content="https://cdn.example/default.jpg">
|
||||
<div itemprop="articleBody"><img width="1" height="1" src="https://cdn.example/pixel.gif">
|
||||
<img src="data:image/gif;base64,AA" data-src="/photo.jpg"></div>`;
|
||||
assert.equal(extractNewsMorningSourceImage(html, 'https://news.example/story'), 'https://news.example/photo.jpg');
|
||||
});
|
||||
|
||||
test('source image fallback does not read images from scripts or comments', () => {
|
||||
const html = `<script>const x='<div class="left_zw"><img src="https://cdn.example/fake.jpg"></div>';</script>
|
||||
<!-- <div class="left_zw"><img src="https://cdn.example/fake.jpg"></div> -->
|
||||
<div class="left_zw"><p>这是一条没有照片的新闻</p></div>
|
||||
<img src="https://cdn.example/related.jpg">`;
|
||||
assert.equal(extractNewsMorningSourceImage(html, 'https://news.example/story'), '');
|
||||
});
|
||||
|
||||
test('image probe forwards article referer and falls back from forbidden HEAD to GET', async () => {
|
||||
const requests = [];
|
||||
let cancelled = 0;
|
||||
const ok = await isFetchableNewsMorningImageUrl('https://cdn.example/photo.jpg', {
|
||||
referrer: 'https://news.example/story',
|
||||
fetchImpl: async (_url, options) => {
|
||||
requests.push(options);
|
||||
return { ok: options.method !== 'HEAD', status: options.method === 'HEAD' ? 403 : 200,
|
||||
body: { cancel: async () => { cancelled += 1; } } };
|
||||
},
|
||||
});
|
||||
assert.equal(ok, true);
|
||||
assert.equal(requests.length, 2);
|
||||
assert.ok(requests.every((options) => options.headers.Referer === 'https://news.example/story'));
|
||||
assert.equal(cancelled, 2);
|
||||
});
|
||||
|
||||
test('a repeated enrichment retains already localized news photos without remote fetches', async () => {
|
||||
const html = '<div class="card"><div class="media"><img src="daily-news-1018-img-story.jpg"></div>'
|
||||
+ '<div class="body"><h3>有本地配图的新闻</h3><div class="source"><a href="https://news.example/story">来源</a></div></div></div>';
|
||||
const result = await enrichNews002HtmlWithSourceImages(html, {
|
||||
fetchImpl: async () => { throw new Error('must not fetch an already localized image'); },
|
||||
});
|
||||
assert.equal(result.resolvedCount, 1);
|
||||
assert.match(result.html, /src="daily-news-1018-img-story.jpg"/);
|
||||
});
|
||||
|
||||
test('enrichNews002HtmlWithSourceImages inserts media and body wrappers', async () => {
|
||||
const source = `<!doctype html><html><body>
|
||||
<div class="masthead"><h1>每日新闻早报</h1></div>
|
||||
@@ -86,6 +138,8 @@ test('enrichNews002HtmlWithSourceImages inserts media and body wrappers', async
|
||||
assert.deepEqual(auditNews002ImageCoverage(result.html), {
|
||||
ok: true,
|
||||
violations: [],
|
||||
qualityWarnings: [],
|
||||
nonHomepageSourceCount: 4,
|
||||
cardCount: 4,
|
||||
imageCount: 4,
|
||||
leadCount: 3,
|
||||
@@ -101,6 +155,7 @@ test('auditNews002ImageCoverage blocks excess cards but keeps imageless stories'
|
||||
)).join('');
|
||||
const audit = auditNews002ImageCoverage(
|
||||
`<div class="masthead"></div>${cards}`,
|
||||
{ maxStoryCards: 28 },
|
||||
);
|
||||
assert.equal(audit.ok, false);
|
||||
assert.ok(audit.violations.includes('too-many-story-cards'));
|
||||
@@ -180,6 +235,8 @@ test('auditNews002ImageCoverage ignores default thumbnails and does not require
|
||||
assert.deepEqual(auditNews002ImageCoverage(html), {
|
||||
ok: true,
|
||||
violations: [],
|
||||
qualityWarnings: ['no-story-images'],
|
||||
nonHomepageSourceCount: 3,
|
||||
cardCount: 4,
|
||||
imageCount: 0,
|
||||
leadCount: 3,
|
||||
@@ -311,7 +368,10 @@ test('localizeNews002CardImages downloads remote images onto the publish host',
|
||||
test('materializeNews002CardFallbackImage copies the shared default thumbnail once', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'news002-fallback-'));
|
||||
try {
|
||||
const filename = materializeNews002CardFallbackImage(tmp);
|
||||
const assets = path.join(tmp, 'wechat/assets');
|
||||
fs.mkdirSync(assets, { recursive: true });
|
||||
fs.writeFileSync(path.join(assets, NEWS002_CARD_FALLBACK_FILENAME), '<svg xmlns="http://www.w3.org/2000/svg"/>');
|
||||
const filename = materializeNews002CardFallbackImage(tmp, { memindLibRoot: tmp });
|
||||
assert.equal(filename, NEWS002_CARD_FALLBACK_FILENAME);
|
||||
assert.equal(fs.existsSync(path.join(tmp, filename)), true);
|
||||
assert.match(fs.readFileSync(path.join(tmp, filename), 'utf8'), /svg/i);
|
||||
@@ -319,3 +379,15 @@ test('materializeNews002CardFallbackImage copies the shared default thumbnail on
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('all-homepage sources and zero images warn without blocking publication', () => {
|
||||
const html = '<div class="masthead"></div><div class="section" id="headlines">'
|
||||
+ ['https://www.xinhuanet.com/', 'https://www.people.com.cn/index.html', 'https://www.thepaper.cn/']
|
||||
.map((url, i) => `<a class="lead-card" href="${url}" data-event-key="e${i}"><h3>新闻${i}</h3></a>`).join('')
|
||||
+ '</div>';
|
||||
const audit = auditNews002ImageCoverage(html);
|
||||
assert.equal(audit.ok, true);
|
||||
assert.deepEqual(audit.violations, []);
|
||||
assert.deepEqual(audit.qualityWarnings, ['no-story-images', 'no-nonhomepage-source-links']);
|
||||
assert.equal(audit.nonHomepageSourceCount, 0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user