import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; const root = new URL("../", import.meta.url); const text = async (path) => readFile(new URL(path, root), "utf8"); test("独立首页只展示家纺情报并加载独立入口", async () => { const html = await text("public/index.html"); const dongguanCss = await text("public/assets/fonts/dongguan-bold/result.css"); assert.match(html, /三笙AI情报局/); assert.match(html, /assets\/js\/app\.js/); assert.match(html, /assets\/js\/app\.js\?v=20260817a/); assert.match(html, /assets\/css\/base\.css\?v=20260814h/); assert.match(html, /assets\/css\/intelligence\.css\?v=20260817a/); assert.match(html, /]+class="intelligence-metrics"/); assert.match(html, /]+class="intelligence-utility-row"/); assert.doesNotMatch(html, /id="intelligenceShareBtn"/); assert.match(html, /class="tool-icon"/); assert.match(html, /assets\/fonts\/dongguan-bold\/result\.css/); assert.doesNotMatch(html, /cn-font\.claude-code-best\.win/); assert.match(dongguanCss, /font-family:"STDongGuanTi Bld"/); assert.match(dongguanCss, /\.woff2/); assert.doesNotMatch(html, />↗<|>◐ { const app = await text("public/assets/js/app.js"); assert.match(app, /\?t=\$\{Date\.now\(\)\}/); assert.match(app, /cache:\s*"no-store"/); assert.doesNotMatch(app, /cache:\s*"no-cache"/); }); test("七个业务入口与趋势四子类均保留", async () => { const renderer = await text("public/assets/js/render-intelligence.js"); const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js"); for (const label of ["今日必看", "备货日历", "展会通", "政策预警", "趋势雷达", "平台渠道", "跨境活动"]) { assert.match(modules, new RegExp(label)); } for (const label of ["灵感图库", "趋势博客", "时装趋势", "图案灵感检索"]) { assert.match(modules, new RegExp(label)); } assert.doesNotMatch(modules, /设计师检索|设计师图片检索台/); assert.match(modules, /appendPrefixedTranslationLink/); assert.match(modules, /appendPrefixedTranslationLink\(actions, item, "seller"\)/); assert.doesNotMatch(modules, /appendPrefixedTranslationLink\(actions, item, "official"\)/); assert.doesNotMatch(modules, /appendPrefixedTranslationLink\(actions, item, "homepage"\)/); assert.doesNotMatch(modules, /查看家纺类目/); assert.doesNotMatch(modules, /家纺类目成熟度|新兴渠道参考分/); assert.match(modules, /打开平台官网/); assert.match(modules, /去官方入驻/); assert.match(modules, /辅助翻译入驻说明/); assert.match(modules, /官方/); assert.match(modules, /requires-cross-border-network/); }); test("所有外部业务链接使用新窗口和安全rel", async () => { const files = [ await text("public/assets/js/render-intelligence.js"), await text("public/assets/js/render-intelligence-core.js"), await text("public/assets/js/render-intelligence-scenes.js"), await text("public/assets/js/render-intelligence-platforms.js"), await text("public/assets/js/render-intelligence-activities.js"), ].join("\n"); assert.match(files, /target\s*=\s*["']_blank["']/); assert.match(files, /noopener noreferrer/); }); test("趋势分类读取真实 items 数组而不是分类对象", async () => { const renderer = await text("public/assets/js/render-intelligence.js"); const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js"); assert.match(modules, /category\.items/); assert.match(modules, /item\.image\s*\|\|\s*item\.thumbnail/); assert.match(modules, /function renderCalendarMonthGrid/); assert.match(modules, /calendar-month-grid/); assert.match(modules, /calendar-weekday/); assert.match(modules, /create\("details", "calendar-local-names"\)/); assert.match(modules, /calendar-local-names-summary/); assert.match(modules, /item\.title_english/); assert.match(modules, /个国家\/地区/); const calendarCss = await text("public/assets/css/intelligence.css"); assert.match(calendarCss, /\.calendar-local-names:not\(\[open\]\) \.calendar-local-name-list\s*\{[^}]*display:\s*none/s); assert.match(calendarCss, /\.calendar-local-summary-name\s*\{[^}]*white-space:\s*nowrap/s); assert.match(modules, /registration_translation_url/); assert.match(modules, /registration_translation_fallback_url/); }); test("灵感图库点击缩略图时使用原图浮层", async () => { const core = await text("public/assets/js/render-intelligence-core.js"); const scenes = await text("public/assets/js/render-intelligence-scenes.js"); const css = await text("public/assets/css/intelligence.css"); assert.match(scenes, /item\.image_original\s*\|\|\s*item\.thumbnail/); assert.match(scenes, /trend-gallery-zoom/); assert.match(scenes, /openLightbox\([^,]+,\s*item\.title/); assert.match(scenes, /previewSrc:\s*item\.thumbnail/); assert.match(core, /role["']?,?\s*["']dialog/); assert.match(core, /aria-modal/); assert.match(core, /assets\/logo\.svg/); assert.match(core, /lightbox-loader/); assert.match(core, /lightbox-retry/); assert.match(core, /lightbox-download/); assert.match(core, /\.decode\(\)/); assert.match(core, /download\s*=/); assert.match(core, /fetch\(src/); assert.match(core, /URL\.createObjectURL/); assert.match(core, /window\.open\(src/); assert.match(css, /\.lightbox-overlay/); assert.match(css, /\.lightbox-preview/); assert.match(css, /\.lightbox-loader-logo-wrap::before\s*\{\s*animation:\s*lightbox-logo-spin\s+1\.8s\s+linear\s+infinite\s*!important;/); assert.match(css, /@media\s*\(prefers-reduced-motion:\s*reduce\)/); }); test("网页翻译严格按 IP 门控", async () => { const region = await text("public/assets/js/visitor-region.js"); assert.match(region, /AbortController/); assert.match(region, /\["\/cdn-cgi\/trace",\s*"https:\/\/www\.cloudflare\.com\/cdn-cgi\/trace"\]/); assert.match(region, /nonChineseLanguage\s*\|\|\s*outsideChinaTimeZone/); const { translationProvider } = await import(new URL("../public/assets/js/visitor-region.js", import.meta.url)); assert.equal(translationProvider("CN", true), "yandex"); assert.equal(translationProvider("US", true), "google"); assert.equal(translationProvider("US", false), "google"); assert.equal(translationProvider("UNKNOWN", true), "google"); assert.equal(translationProvider("UNKNOWN", false), "yandex"); }); test("官方来源位于译文入口之前且可自动退到可用翻译服务", async () => { const core = await text("public/assets/js/render-intelligence-core.js"); const scenes = await text("public/assets/js/render-intelligence-scenes.js"); const activities = await text("public/assets/js/render-intelligence-activities.js"); const seoBuilder = await text("scripts/build_seo.py"); const seoRegion = await text("public/assets/js/seo-visitor-region.js"); const policy = scenes.slice(scenes.indexOf("function makePolicyCard"), scenes.indexOf("function renderPolicies")); assert.ok(policy.indexOf("sourceLinkLabel(item)") < policy.indexOf("appendTranslationLinks(actions, item)")); assert.match(core, /function preferredTranslation/); assert.match(activities, /googleUrl \|\| yandexUrl/); assert.match(activities, /yandexUrl \|\| googleUrl/); assert.doesNotMatch(`${core}\n${activities}`, /百度中文|translationProvider === "baidu"/); assert.match(seoBuilder, /data-translation-group/); assert.match(seoRegion, /links\[0\]/); }); test("跨境活动不把发现栏目页冒充具体来源或译文", async () => { const activities = await text("public/assets/js/render-intelligence-activities.js"); assert.doesNotMatch(activities, /source_translation_url|source_translation_fallback_url/); assert.match(activities, /sourceUrl !== registrationUrl/); assert.match(activities, /activityDayOffset\(item\.start_date\) >= 0/); }); test("跨境活动按访问当天重新计算相对日期并排除已结束活动", async () => { const { activityDayOffset } = await import(new URL("../public/assets/js/render-intelligence-activities.js", import.meta.url)); const now = new Date(2026, 7, 14, 12, 0, 0); assert.equal(activityDayOffset("2026-08-13", now), -1); assert.equal(activityDayOffset("2026-08-14", now), 0); assert.equal(activityDayOffset("2026-08-15", now), 1); }); test("日历按当前 UTC 日期优先展示未过期节点并把已过节点压到后面", async () => { const { calendarTodayIso, sortCalendarItems, calendarDefaultItems } = await import(new URL(`../public/assets/js/render-intelligence-core.js?calendar-date=${Date.now()}`, import.meta.url)); const now = new Date("2026-08-15T00:30:00.000Z"); const items = [ { id: "july", start_date: "2026-07-15", priority_score: 90 }, { id: "past-august", start_date: "2026-08-10", priority_score: 90 }, { id: "today", start_date: "2026-08-15", priority_score: 1 }, { id: "future", start_date: "2026-08-17", priority_score: 1 }, ]; assert.equal(calendarTodayIso(now), "2026-08-15"); assert.deepEqual(sortCalendarItems(items, now).map((item) => item.id), ["today", "future", "past-august", "july"]); assert.deepEqual(calendarDefaultItems(items, 2026, now).map((item) => item.id), ["today", "future"]); }); test("最新家纺交互与移动端契约已同步", async () => { const renderer = await text("public/assets/js/render-intelligence.js"); const modules = renderer + await text("public/assets/js/render-intelligence-core.js") + await text("public/assets/js/render-intelligence-scenes.js") + await text("public/assets/js/render-intelligence-platforms.js"); const baseCss = await text("public/assets/css/base.css"); const css = await text("public/assets/css/intelligence.css"); assert.doesNotMatch(renderer, /displayLimit/); assert.match(modules, /data\.calendar_insights\?\.length \|\| data\.calendar\?\.length/); assert.match(modules, /platform-filter-toolbar/); const activities = await text("public/assets/js/render-intelligence-activities.js"); assert.match(modules + activities, /activity-filter-toolbar/); assert.match(activities, /activity-filter-fields/); assert.match(css, /\.activity-focus-switch\s*\{[^}]*grid-template-columns:\s*repeat\(4,\s*minmax\(0,\s*1fr\)\)/s); assert.match(css, /\.activity-focus-btn\s*\{[^}]*min-height:\s*40px/s); assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.activity-focus-switch\s*\{[^}]*grid-template-columns:\s*1fr 1fr/s); assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.activity-focus-btn\s*\{[^}]*min-height:\s*48px/s); assert.doesNotMatch(modules, /家纺成熟度|新兴参考/); assert.doesNotMatch(modules, /scope_type === "state\.region"|\["state\.region", "地区政策"\]/); assert.match(modules, /scope_type === "region"/); assert.match(renderer, /closest\("label"\)/); assert.match(renderer, /regionField\.hidden\s*=\s*state\.activeSection === "calendar"/); assert.match(css, /@media \(max-width: 680px\)/); assert.match(css, /\.intelligence-tabs\s*\{[^}]*grid-template-columns: repeat\(3/); assert.match(css, /min-height: 48px/); for (const family of ["STDongGuanTi Light", "STDongGuanTi", "STDongGuanTi Bld"]) assert.match(baseCss, new RegExp(family)); assert.match(activities, /activity-date-month/); assert.match(activities, /activity-date-day/); assert.match(activities, /activity-date-countdown/); }); test("地址栏状态不会写入 undefined", async () => { const app = await text("public/assets/js/app.js"); assert.match(app, /value \|\| getIntelligenceSection\(\)/); }); test("东观体只用于标题且窄卡海报为日历保留空间", async () => { const baseCss = await text("public/assets/css/base.css"); const css = await text("public/assets/css/intelligence.css"); assert.match(baseCss, /body\s*\{[^}]*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", sans-serif/); assert.doesNotMatch(baseCss, /body\s*\{[^}]*font-family: Inter/); assert.match(baseCss, /\.brand strong\s*\{[^}]*font-family: inherit[^}]*font-weight: 700/); assert.match(baseCss, /\.page-heading h1\s*\{[^}]*font-family: inherit[^}]*font-size: clamp\(30px, 3vw, 40px\)[^}]*font-weight: 700/); assert.match(css, /\.site-shell\.textile-wrap\s*\{[^}]*width: min\(82vw, 1680px\)/); assert.match(css, /@media \(max-width: 680px\)[\s\S]*?\.site-shell\.textile-wrap\s*\{[^}]*width: calc\(100% - 24px\)/); assert.doesNotMatch(baseCss, /\.brand small\s*\{[^}]*font-family:/); assert.doesNotMatch(baseCss, /\.page-heading > p\s*\{[^}]*font-family:/); assert.match(css, /\.intelligence-poster-title\s*\{[^}]*font-family: var\(--textile-editorial-font-bold\)/s); assert.match(css, /\.activity-card-body h3\s*\{[^}]*font-family: var\(--textile-editorial-font-bold\)/); assert.doesNotMatch(css, /\.activity-generated-poster small, \.activity-poster-source\s*\{[^}]*font-family:/); assert.doesNotMatch(css, /\.activity-date-(?:month|day|countdown)\s*\{[^}]*font-family:/); assert.doesNotMatch(css, /\.activity-(?:title-original|summary)[^\{]*\{[^}]*font-family:/); assert.match(css, /\.activity-card-visual\s*\{[^}]*width: 100%[^}]*min-height: 220px/); });