官网改版上线:关于我们与联系我们页重构、首页AI主视觉v4、品牌标识与导航交互;同步测试与设计文档
这个提交包含在:
62
tests/contact_form_browser.js
普通文件
62
tests/contact_form_browser.js
普通文件
@@ -0,0 +1,62 @@
|
||||
// Run on the local contact preview with agent-browser eval. All requests are mocked.
|
||||
(async function () {
|
||||
if (!location.hostname.match(/^(127\.0\.0\.1|localhost)$/)) throw new Error('Use local preview only');
|
||||
var form = document.querySelector('.ct-form');
|
||||
var submit = form.querySelector('.ct-submit');
|
||||
var status = form.querySelector('.ct-submit-status');
|
||||
var originalFetch = window.fetch;
|
||||
var calls = [];
|
||||
var mode = 'reject';
|
||||
var results = [];
|
||||
function assert(condition, name) { if (!condition) throw new Error(name); results.push(name); }
|
||||
window.fetch = async function (url, options) {
|
||||
calls.push({url: String(url), method: options.method || 'GET', body: options.body || ''});
|
||||
if (!options.method) {
|
||||
if (mode === 'token-fail') throw new Error('network');
|
||||
return {ok: true, json: async function () { return {code: 1, data: 'test-single-use-token'}; }};
|
||||
}
|
||||
if (mode === 'network-fail') throw new Error('network');
|
||||
await new Promise(function (resolve) { setTimeout(resolve, 10); });
|
||||
return {ok: true, json: async function () { return mode === 'success' ? {code: 1} : {code: 0, msg: '验证码不正确'}; }};
|
||||
};
|
||||
function fill() {
|
||||
form.elements.title.value = '本地界面测试';
|
||||
form.elements.name.value = '测试';
|
||||
form.elements.phone.value = '0851-12345678';
|
||||
form.elements.content.value = '本地模拟数据,不会向线上发送。';
|
||||
if (form.elements.captcha) form.elements.captcha.value = 'test';
|
||||
}
|
||||
function send() { form.dispatchEvent(new Event('submit', {bubbles: true, cancelable: true})); }
|
||||
async function settled() {
|
||||
for (var i = 0; i < 100; i++) {
|
||||
if (form.getAttribute('aria-busy') !== 'true') return;
|
||||
await new Promise(function (resolve) { setTimeout(resolve, 5); });
|
||||
}
|
||||
throw new Error('Form did not settle');
|
||||
}
|
||||
try {
|
||||
form.reset(); send();
|
||||
assert(calls.length === 0 && document.activeElement === form.elements.title, 'Empty form: focus first error, no request');
|
||||
form.querySelector('[data-topic]').click();
|
||||
assert(form.elements.title.value === form.querySelector('[data-topic]').dataset.topic, 'Topic button fills title');
|
||||
fill(); form.elements.phone.value = '123'; send();
|
||||
assert(calls.length === 0 && form.elements.phone.getAttribute('aria-invalid') === 'true', 'Invalid phone blocked');
|
||||
fill(); send(); send();
|
||||
assert(submit.disabled, 'Submitting state disables button');
|
||||
await settled();
|
||||
assert(calls.filter(function (c) { return c.method === 'POST'; }).length === 1, 'Duplicate submit sends one POST');
|
||||
var body = new URLSearchParams(calls.find(function (c) { return c.method === 'POST'; }).body);
|
||||
assert(body.get('__token__') === 'test-single-use-token' && body.get('phone') === '0851-12345678', 'Uses fresh token and expected backend fields');
|
||||
assert(status.dataset.state === 'error' && form.elements.content.value.length > 0 && !submit.disabled, 'Server rejection preserves fields and permits retry');
|
||||
if (form.elements.captcha) assert(form.elements.captcha.value === '', 'Rejected submission refreshes captcha');
|
||||
calls = []; fill(); mode = 'success'; send(); await settled();
|
||||
assert(status.dataset.state === 'success' && form.elements.name.value === '' && form.elements.content.value === '', 'Confirmed success clears fields and announces result');
|
||||
assert(calls.length === 2 && calls[0].method === 'GET', 'Retry obtains a new token');
|
||||
calls = []; fill(); mode = 'network-fail'; send(); await settled();
|
||||
assert(status.dataset.state === 'error' && status.textContent.includes('无法确认提交结果') && form.elements.content.value.length > 0, 'Unknown POST outcome preserves data without automatic retry');
|
||||
assert(calls.length === 2 && !submit.disabled, 'Network failure releases button');
|
||||
calls = []; fill(); mode = 'token-fail'; send(); await settled();
|
||||
assert(calls.length === 1 && status.dataset.state === 'error', 'Token fetch failure never sends POST');
|
||||
return {passed: results.length, checks: results, network: 'Mocked locally; no real messages sent'};
|
||||
} finally { window.fetch = originalFetch; }
|
||||
})();
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
中华人民共和国广告法文案合规自动化专项测试
|
||||
(Ad Law Compliance Test Suite)
|
||||
@@ -34,7 +33,7 @@ class TestAdLawCompliance(unittest.TestCase):
|
||||
self.contents = {}
|
||||
for key, path in self.target_files.items():
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
with open(path, encoding='utf-8') as f:
|
||||
self.contents[key] = f.read()
|
||||
else:
|
||||
self.contents[key] = ""
|
||||
@@ -56,9 +55,10 @@ class TestAdLawCompliance(unittest.TestCase):
|
||||
# 严禁出现“国家级认定”、“国家级自主研发”等违法表述
|
||||
self.assertNotIn('国家级自主研发', self.all_text, "不得使用'国家级自主研发'违规修饰")
|
||||
self.assertNotIn('国家级科技创新认定', self.all_text, "不得使用'国家级科技创新认定'违规表述")
|
||||
# 合规的表达应当为官方备案机构或高新技术企业全称
|
||||
self.assertIn('全国高新技术企业认定管理机构备案认定', self.contents['single_pc'],
|
||||
"资质表述须符合法定规范,应使用全国高新技术企业认定管理机构备案认定")
|
||||
# 合规的表达应当使用法定全称,并以有效资料与证书为准
|
||||
for page_name, content in (('PC 关于我们页', self.contents['single_pc']), ('移动端关于我们页', self.contents['single_m'])):
|
||||
self.assertIn('计算机软件著作权', content, f"{page_name}资质表述应使用法定全称")
|
||||
self.assertIn('以有效证书为准', content, f"{page_name}资质表述应以有效证书为准")
|
||||
|
||||
def test_no_absolute_guarantee_and_exaggeration(self):
|
||||
"""广告法第四条/第二十八条:严禁绝对化与夸大免责承诺"""
|
||||
@@ -73,7 +73,7 @@ class TestAdLawCompliance(unittest.TestCase):
|
||||
clean_text = re.sub(r'[\'\"a-zA-Z0-9_\-:]*\s*:\s*100%', '', self.all_text)
|
||||
clean_text = re.sub(r'width\s*=\s*[\'\"]100%[\'\"]', '', clean_text)
|
||||
clean_text = re.sub(r'height\s*=\s*[\'\"]100%[\'\"]', '', clean_text)
|
||||
|
||||
|
||||
self.assertNotIn('准确率: <strong>100%</strong>', clean_text, "分账准确率不得夸大承诺100%")
|
||||
self.assertNotIn('自动对账率: <strong>100%</strong>', clean_text, "对账率不得夸大承诺100%")
|
||||
self.assertNotIn('100% 完整源码', clean_text, "应表述为完整工程源码交付,避免100%绝对词")
|
||||
@@ -133,7 +133,7 @@ class TestAdLawCompliance(unittest.TestCase):
|
||||
"""排查 public/assets/addons/official/images/showcases/ 目录下所有原型 SVG 是否包含虚假宣传数据"""
|
||||
svg_dir = os.path.join(ROOT_DIR, 'public/assets/addons/official/images/showcases')
|
||||
self.assertTrue(os.path.exists(svg_dir), f"SVG 展示目录不存在: {svg_dir}")
|
||||
|
||||
|
||||
# 针对每个 SVG 定义违规关键词
|
||||
svg_forbidden_rules = {
|
||||
'case_distrib.svg': [
|
||||
@@ -168,7 +168,7 @@ class TestAdLawCompliance(unittest.TestCase):
|
||||
for filename, forbidden_list in svg_forbidden_rules.items():
|
||||
svg_path = os.path.join(svg_dir, filename)
|
||||
if os.path.exists(svg_path):
|
||||
with open(svg_path, 'r', encoding='utf-8') as f:
|
||||
with open(svg_path, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
for forbidden in forbidden_list:
|
||||
self.assertNotIn(forbidden, content, f"SVG 文件 {filename} 中严禁包含虚假宣传或夸大编造数据: '{forbidden}'")
|
||||
|
||||
@@ -41,18 +41,13 @@ class TestClientRigorEnhancements(unittest.TestCase):
|
||||
"案例独立页必须包含明确的验收与生产运行记录")
|
||||
|
||||
def test_authoritative_certifications_and_copyright_numbers(self):
|
||||
"""关于我们资质板块必须展示规整的资质体系与等保合规"""
|
||||
# ISO 与 等保
|
||||
self.assertIn('ISO9001', self.about_html, "PC 关于我们必须包含 ISO9001 质量认证")
|
||||
self.assertIn('ISO27001', self.about_html, "PC 关于我们必须包含 ISO27001 安全认证")
|
||||
self.assertTrue('等保' in self.about_html, "PC 关于我们必须包含等保合规设计")
|
||||
# 软著与知识产权
|
||||
self.assertTrue('计算机软件著作权' in self.about_html and '自主可控' in self.about_html,
|
||||
"PC 关于我们必须展示自主可控的计算机软件著作权与知识产权")
|
||||
|
||||
# 移动端同步
|
||||
self.assertIn('ISO', self.about_mobile, "移动端关于我们必须同步展示权威资质")
|
||||
self.assertTrue('等保' in self.about_mobile, "移动端关于我们必须同步展示等保合规")
|
||||
"""资质和软件权属应说明核验依据,不以示意徽章冒充认证。"""
|
||||
for content in (self.about_html, self.about_mobile):
|
||||
for label in ('企业主体', '计算机软件著作权', '第三方组件', '合同', 'ISO9001', 'ISO27001', '等保'):
|
||||
self.assertIn(label, content)
|
||||
self.assertIn('具体认证情况以有效证书为准', content)
|
||||
self.assertNotIn('cert_hi_tech.svg', content)
|
||||
self.assertNotIn('cert_iso9001.svg', content)
|
||||
|
||||
def test_demo_sandbox_rigorous_commitment(self):
|
||||
"""产品页演示沙箱必须具备极速建联与真实脱敏环境承诺"""
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
2. 验证新版服务页摄影配图真实存在、尺寸满足首屏展示要求,并提供 WebP 兼容版本;
|
||||
3. 验证联系我们页矢量配图符合浅色专业风规范。
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
@@ -12,54 +13,86 @@ from PIL import Image
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
RASTER_ROOT = os.path.join(ROOT_DIR, 'public', 'assets', 'addons', 'official', 'images')
|
||||
SHOWCASE_ROOT = os.path.join(RASTER_ROOT, 'showcases')
|
||||
RASTER_ROOT = os.path.join(ROOT_DIR, "public", "assets", "addons", "official", "images")
|
||||
SHOWCASE_ROOT = os.path.join(RASTER_ROOT, "showcases")
|
||||
|
||||
SERVICE_HEROES = {
|
||||
'software': ('software/structure-v2.jpg', 1440, 960),
|
||||
'app': ('app/terminals.jpg', 1440, 960),
|
||||
'agent': ('agent/landscape-v2.jpg', 1920, 640),
|
||||
"software": ("software/structure-v2.jpg", 1440, 960),
|
||||
"app": ("app/terminals.jpg", 1440, 960),
|
||||
"agent": ("agent/landscape-v2.jpg", 1920, 640),
|
||||
"contact": ("contact/connection.jpg", 1440, 960),
|
||||
}
|
||||
|
||||
|
||||
class TestDedicatedPageIllustrations(unittest.TestCase):
|
||||
|
||||
def test_illustration_assets_exist(self):
|
||||
"""测试专属配图文件存在、格式有效且尺寸满足展示要求"""
|
||||
for key, (relative_path, minimum_width, minimum_height) in SERVICE_HEROES.items():
|
||||
for key, (
|
||||
relative_path,
|
||||
minimum_width,
|
||||
minimum_height,
|
||||
) in SERVICE_HEROES.items():
|
||||
with self.subTest(page=key):
|
||||
jpg_path = os.path.join(RASTER_ROOT, relative_path)
|
||||
webp_path = os.path.splitext(jpg_path)[0] + '.webp'
|
||||
self.assertTrue(os.path.isfile(jpg_path), f"缺少专属精美配图: {relative_path}")
|
||||
self.assertTrue(os.path.isfile(webp_path), f"缺少 WebP 兼容版本: {relative_path}")
|
||||
webp_path = os.path.splitext(jpg_path)[0] + ".webp"
|
||||
self.assertTrue(
|
||||
os.path.isfile(jpg_path), f"缺少专属精美配图: {relative_path}"
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(webp_path), f"缺少 WebP 兼容版本: {relative_path}"
|
||||
)
|
||||
with Image.open(jpg_path) as hero:
|
||||
width, height = hero.size
|
||||
self.assertGreaterEqual(width, minimum_width, f"{relative_path} 宽度不足首屏要求")
|
||||
self.assertGreaterEqual(height, minimum_height, f"{relative_path} 高度不足首屏要求")
|
||||
self.assertGreater(os.path.getsize(jpg_path), 50_000, f"{relative_path} 文件过小")
|
||||
self.assertGreaterEqual(
|
||||
width, minimum_width, f"{relative_path} 宽度不足首屏要求"
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
height, minimum_height, f"{relative_path} 高度不足首屏要求"
|
||||
)
|
||||
self.assertGreater(
|
||||
os.path.getsize(jpg_path), 50_000, f"{relative_path} 文件过小"
|
||||
)
|
||||
|
||||
contact_path = os.path.join(SHOWCASE_ROOT, 'contact_headquarters.svg')
|
||||
with open(contact_path, encoding='utf-8') as stream:
|
||||
contact_path = os.path.join(SHOWCASE_ROOT, "contact_headquarters.svg")
|
||||
with open(contact_path, encoding="utf-8") as stream:
|
||||
contact_content = stream.read()
|
||||
self.assertIn('<svg', contact_content, 'contact_headquarters.svg 必须是有效矢量 SVG')
|
||||
self.assertIn(
|
||||
"<svg", contact_content, "contact_headquarters.svg 必须是有效矢量 SVG"
|
||||
)
|
||||
|
||||
def test_pages_embed_dedicated_illustrations(self):
|
||||
"""测试各独立页面均正确嵌入对应的精美配图"""
|
||||
pages = {
|
||||
'software': ('official_product/service_software.html', 'images/software/structure-v2.jpg'),
|
||||
'app': ('official_product/service_app.html', 'images/app/terminals.jpg'),
|
||||
'agent': ('official_product/service_agent.html', 'images/agent/landscape-v2.jpg'),
|
||||
'contact_pc': ('official_online_message/list.html', 'contact_headquarters.svg'),
|
||||
'contact_m': ('official_online_message/list_e.html', 'contact_headquarters.svg'),
|
||||
"software": (
|
||||
"official_product/service_software.html",
|
||||
"images/software/structure-v2.jpg",
|
||||
),
|
||||
"app": ("official_product/service_app.html", "images/app/terminals.jpg"),
|
||||
"agent": (
|
||||
"official_product/service_agent.html",
|
||||
"images/agent/landscape-v2.jpg",
|
||||
),
|
||||
"contact_pc": (
|
||||
"official_online_message/list.html",
|
||||
"images/contact/connection.jpg",
|
||||
),
|
||||
"contact_m": (
|
||||
"official_online_message/list_e.html",
|
||||
"images/contact/connection.jpg",
|
||||
),
|
||||
}
|
||||
template_dir = os.path.join(ROOT_DIR, 'addons', 'official', 'view', 'web')
|
||||
template_dir = os.path.join(ROOT_DIR, "addons", "official", "view", "web")
|
||||
for key, (template_relative_path, image_name) in pages.items():
|
||||
with self.subTest(page=key):
|
||||
template_path = os.path.join(template_dir, template_relative_path)
|
||||
with open(template_path, encoding='utf-8') as stream:
|
||||
with open(template_path, encoding="utf-8") as stream:
|
||||
html = stream.read()
|
||||
self.assertIn(image_name, html, f"页面 {template_relative_path} 必须正确引用专属配图 {image_name}")
|
||||
self.assertIn(
|
||||
image_name,
|
||||
html,
|
||||
f"页面 {template_relative_path} 必须正确引用专属配图 {image_name}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
新闻资讯与技术文章改版及丰富度测试 (TDD 模式)
|
||||
验证文章独立页、移动端模板、首页联动、数据库种子文件及去AI味道规范
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
@@ -16,26 +17,86 @@ def _read_text(path):
|
||||
|
||||
|
||||
class TestJournalismAndArticles(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.pc_list_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_journalism', 'list.html')
|
||||
self.pc_details_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_journalism', 'details.html')
|
||||
self.mobile_list_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_journalism', 'list_e.html')
|
||||
self.mobile_details_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_journalism', 'details_e.html')
|
||||
self.home_index_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_home', 'index.html')
|
||||
self.footer_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_common', 'pc', 'footer.html')
|
||||
self.css_path = os.path.join(BASE_DIR, 'public', 'assets', 'addons', 'official', 'css', 'modern_nhc.css')
|
||||
self.seed_sql_path = os.path.join(BASE_DIR, 'database', 'seeds', 'journalism_articles.sql')
|
||||
self.pc_list_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_journalism",
|
||||
"list.html",
|
||||
)
|
||||
self.pc_details_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_journalism",
|
||||
"details.html",
|
||||
)
|
||||
self.mobile_list_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_journalism",
|
||||
"list_e.html",
|
||||
)
|
||||
self.mobile_details_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_journalism",
|
||||
"details_e.html",
|
||||
)
|
||||
self.home_index_path = os.path.join(
|
||||
BASE_DIR, "addons", "official", "view", "web", "official_home", "index.html"
|
||||
)
|
||||
self.footer_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_common",
|
||||
"pc",
|
||||
"footer.html",
|
||||
)
|
||||
self.css_path = os.path.join(
|
||||
BASE_DIR, "public", "assets", "addons", "official", "css", "modern_nhc.css"
|
||||
)
|
||||
self.seed_sql_path = os.path.join(
|
||||
BASE_DIR, "database", "seeds", "journalism_articles.sql"
|
||||
)
|
||||
|
||||
def test_pc_list_template_spec(self):
|
||||
"""验证 PC 端文章资讯列表页规范(标题、简介、缩略图专业排版)"""
|
||||
self.assertTrue(os.path.exists(self.pc_list_path), "PC 文章列表模板 list.html 必须存在")
|
||||
self.assertTrue(
|
||||
os.path.exists(self.pc_list_path), "PC 文章列表模板 list.html 必须存在"
|
||||
)
|
||||
content = _read_text(self.pc_list_path)
|
||||
self.assertIn("纳海川", content, "必须包含品牌名称【纳海川】")
|
||||
self.assertIsNone(re.search(r'纳川(?!海)', content), "严禁将品牌错误简写为【纳川】")
|
||||
self.assertFalse(re.search(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]', content), "严禁包含任何 emoji")
|
||||
self.assertIn("nhc-article-hero", content, "必须包含现代中式 Hero Banner 容器类")
|
||||
self.assertIn("nhc-news-list", content, "必须包含新闻资讯列表容器 nhc-news-list")
|
||||
self.assertIsNone(
|
||||
re.search(r"纳川(?!海)", content), "严禁将品牌错误简写为【纳川】"
|
||||
)
|
||||
self.assertFalse(
|
||||
re.search(
|
||||
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]",
|
||||
content,
|
||||
),
|
||||
"严禁包含任何 emoji",
|
||||
)
|
||||
self.assertIn(
|
||||
"nhc-article-hero", content, "必须包含现代中式 Hero Banner 容器类"
|
||||
)
|
||||
self.assertIn(
|
||||
"nhc-news-list", content, "必须包含新闻资讯列表容器 nhc-news-list"
|
||||
)
|
||||
self.assertIn("nhc-news-row", content, "必须包含图文行卡片 nhc-news-row")
|
||||
self.assertIn("nhc-news-thumb", content, "必须包含缩略图容器 nhc-news-thumb")
|
||||
self.assertIn("nhc-news-title", content, "必须包含文章标题 nhc-news-title")
|
||||
@@ -44,28 +105,62 @@ class TestJournalismAndArticles(unittest.TestCase):
|
||||
|
||||
def test_pc_details_template_spec(self):
|
||||
"""验证 PC 端文章资讯详情页规范"""
|
||||
self.assertTrue(os.path.exists(self.pc_details_path), "PC 文章详情模板 details.html 必须存在")
|
||||
self.assertTrue(
|
||||
os.path.exists(self.pc_details_path),
|
||||
"PC 文章详情模板 details.html 必须存在",
|
||||
)
|
||||
content = _read_text(self.pc_details_path)
|
||||
self.assertIn("纳海川", content, "必须包含品牌名称【纳海川】")
|
||||
self.assertIsNone(re.search(r'纳川(?!海)', content), "严禁将品牌错误简写为【纳川】")
|
||||
self.assertFalse(re.search(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]', content), "严禁包含任何 emoji")
|
||||
self.assertIsNone(
|
||||
re.search(r"纳川(?!海)", content), "严禁将品牌错误简写为【纳川】"
|
||||
)
|
||||
self.assertFalse(
|
||||
re.search(
|
||||
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]",
|
||||
content,
|
||||
),
|
||||
"严禁包含任何 emoji",
|
||||
)
|
||||
self.assertIn("nhc-article-body", content, "必须包含规范的文章正文排版容器")
|
||||
self.assertIn("nhc-breadcrumb", content, "必须包含严谨的面包屑导航")
|
||||
self.assertIn("info_previous_next_wrap", content, "必须包含上一篇/下一篇自然导流结构")
|
||||
self.assertIn(
|
||||
"info_previous_next_wrap", content, "必须包含上一篇/下一篇自然导流结构"
|
||||
)
|
||||
|
||||
def test_mobile_templates_spec_and_bugfix(self):
|
||||
"""验证移动端模板规范(标题、简介、缩略图专业排版)"""
|
||||
self.assertTrue(os.path.exists(self.mobile_list_path), "移动端列表模板 list_e.html 必须存在")
|
||||
self.assertTrue(os.path.exists(self.mobile_details_path), "移动端详情模板 details_e.html 必须存在")
|
||||
self.assertTrue(
|
||||
os.path.exists(self.mobile_list_path), "移动端列表模板 list_e.html 必须存在"
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.exists(self.mobile_details_path),
|
||||
"移动端详情模板 details_e.html 必须存在",
|
||||
)
|
||||
list_content = _read_text(self.mobile_list_path)
|
||||
details_content = _read_text(self.mobile_details_path)
|
||||
|
||||
self.assertIn("nhc-news-thumb", list_content, "移动端列表必须包含缩略图结构")
|
||||
self.assertIn("nhc-news-title", list_content, "移动端列表必须包含标题结构")
|
||||
self.assertIn("nhc-news-desc", list_content, "移动端列表必须包含简介结构")
|
||||
self.assertNotIn("official_video/list", details_content, "详情页严禁遗留旧 official_video/list 错误路径")
|
||||
self.assertFalse(re.search(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]', list_content), "移动端列表严禁 emoji")
|
||||
self.assertFalse(re.search(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]', details_content), "移动端详情严禁 emoji")
|
||||
self.assertNotIn(
|
||||
"official_video/list",
|
||||
details_content,
|
||||
"详情页严禁遗留旧 official_video/list 错误路径",
|
||||
)
|
||||
self.assertFalse(
|
||||
re.search(
|
||||
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]",
|
||||
list_content,
|
||||
),
|
||||
"移动端列表严禁 emoji",
|
||||
)
|
||||
self.assertFalse(
|
||||
re.search(
|
||||
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\u2600-\u26FF\u2700-\u27BF]",
|
||||
details_content,
|
||||
),
|
||||
"移动端详情严禁 emoji",
|
||||
)
|
||||
|
||||
def test_css_design_tokens_for_articles(self):
|
||||
"""验证 CSS 中已包含文章与资讯专属图文排版样式定义"""
|
||||
@@ -78,26 +173,47 @@ class TestJournalismAndArticles(unittest.TestCase):
|
||||
|
||||
def test_seed_sql_rich_articles(self):
|
||||
"""验证数据库种子包含不少于 38 篇扎实深度专业文章"""
|
||||
self.assertTrue(os.path.exists(self.seed_sql_path), "数据库种子文件 journalism_articles.sql 必须存在")
|
||||
self.assertTrue(
|
||||
os.path.exists(self.seed_sql_path),
|
||||
"数据库种子文件 journalism_articles.sql 必须存在",
|
||||
)
|
||||
sql = _read_text(self.seed_sql_path)
|
||||
|
||||
# 验证至少 38 篇优质文章插入 (初始 8 篇 + 新增 30 篇)
|
||||
insert_count = (sql.count("INSERT INTO `fa_official_list_journalism`") +
|
||||
sql.count("INSERT INTO fa_official_list_journalism") +
|
||||
sql.count("REPLACE INTO `fa_official_list_journalism`") +
|
||||
sql.count("REPLACE INTO fa_official_list_journalism"))
|
||||
self.assertGreaterEqual(insert_count, 38, f"必须有至少 38 篇高质量文章 SQL 写入语句 (当前: {insert_count})")
|
||||
insert_count = (
|
||||
sql.count("INSERT INTO `fa_official_list_journalism`")
|
||||
+ sql.count("INSERT INTO fa_official_list_journalism")
|
||||
+ sql.count("REPLACE INTO `fa_official_list_journalism`")
|
||||
+ sql.count("REPLACE INTO fa_official_list_journalism")
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
insert_count,
|
||||
38,
|
||||
f"必须有至少 38 篇高质量文章 SQL 写入语句 (当前: {insert_count})",
|
||||
)
|
||||
|
||||
# 验证核心业务主题覆盖
|
||||
self.assertIn("微服务", sql, "必须覆盖微服务架构深度文章")
|
||||
self.assertIn("智能体", sql, "必须覆盖 AI 智能体/大模型与知识库落地文章")
|
||||
self.assertTrue("代码" in sql or "软件质量" in sql, "必须覆盖代码规范与交付文章")
|
||||
self.assertTrue(
|
||||
"代码" in sql or "软件质量" in sql, "必须覆盖代码规范与交付文章"
|
||||
)
|
||||
self.assertIn("缓存", sql, "必须覆盖分布式缓存相关深度文章")
|
||||
self.assertIn("分库分表", sql, "必须覆盖数据库分库分表相关文章")
|
||||
self.assertIn("先进先出", sql, "必须覆盖批次库存先进先出算法相关文章")
|
||||
|
||||
# 严禁假大空与 AI 味营销词汇
|
||||
for bad_word in ["赋能千行百业", "颠覆式创新", "颠覆传统", "未来已来", "数字化新纪元", "技术委员会", "研发委员会", "原创技术资产", "工程受控说明"]:
|
||||
for bad_word in [
|
||||
"赋能千行百业",
|
||||
"颠覆式创新",
|
||||
"颠覆传统",
|
||||
"未来已来",
|
||||
"数字化新纪元",
|
||||
"技术委员会",
|
||||
"研发委员会",
|
||||
"原创技术资产",
|
||||
"工程受控说明",
|
||||
]:
|
||||
self.assertNotIn(bad_word, sql, f"文章种子严禁包含 AI 味空话:{bad_word}")
|
||||
|
||||
def test_home_and_footer_linkage(self):
|
||||
@@ -118,8 +234,13 @@ class TestJournalismAndArticles(unittest.TestCase):
|
||||
self.assertTrue(has_news_section, "首页必须包含技术洞察与研发动态板块")
|
||||
|
||||
# 页脚必须有技术资讯入口
|
||||
has_insights_link = "official_journalism" in footer or "技术洞察" in footer or "新闻资讯" in footer
|
||||
has_insights_link = (
|
||||
"official_journalism" in footer
|
||||
or "技术洞察" in footer
|
||||
or "新闻资讯" in footer
|
||||
)
|
||||
self.assertTrue(has_insights_link, "页脚必须包含技术洞察/新闻资讯链接")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -143,9 +143,14 @@ class TestNaturalCopywriting(unittest.TestCase):
|
||||
)
|
||||
|
||||
# 关于我们与联系我们
|
||||
self.assertIn(
|
||||
"找懂业务、讲规范的研发团队,让软件项目平稳落地",
|
||||
self.about_html,
|
||||
self.assertTrue(
|
||||
any(
|
||||
text in self.about_html
|
||||
for text in (
|
||||
"找懂业务、讲规范的研发团队,让软件项目平稳落地",
|
||||
"沟通您的软件建设需求",
|
||||
)
|
||||
),
|
||||
"关于我们CTA必须务实接地气",
|
||||
)
|
||||
self.assertIn(
|
||||
@@ -286,7 +291,9 @@ class TestNaturalCopywriting(unittest.TestCase):
|
||||
self.assertIn("动态安全库存模型与自动补货采购建议", self.products_html)
|
||||
self.assertIn("企业知识问答", service_agent_html)
|
||||
self.assertIn("业务流程辅助", service_agent_html)
|
||||
self.assertIn("检索增强可以降低错误风险,但不能保证所有回答都正确", service_agent_html)
|
||||
self.assertIn(
|
||||
"检索增强可以降低错误风险,但不能保证所有回答都正确", service_agent_html
|
||||
)
|
||||
self.assertNotIn("不会自己编造", service_agent_html)
|
||||
self.assertNotIn("一张或两张显卡即可", service_agent_html)
|
||||
self.assertIn("根据使用方式,选择合适的终端", service_app_html)
|
||||
|
||||
@@ -5,101 +5,143 @@
|
||||
2. 确保面向客户与访客,使用大白话、实在话(如防重复扣款、源码交付、内网部署数据不泄露、标注文档原文);
|
||||
3. 确保各个独立页面文案短小精悍、不啰嗦、讲人话。
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
class TestPlainAndConciseCopywriting(unittest.TestCase):
|
||||
|
||||
class TestPlainAndConciseCopywriting(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.files = {
|
||||
'index': os.path.join(ROOT_DIR, 'addons/official/view/web/official_home/index.html'),
|
||||
'index_e': os.path.join(ROOT_DIR, 'addons/official/view/web/official_home/index_e.html'),
|
||||
'service_page': os.path.join(ROOT_DIR, 'addons/official/view/web/official_product/service_page.html'),
|
||||
'service_software': os.path.join(ROOT_DIR, 'addons/official/view/web/official_product/service_software.html'),
|
||||
'service_app': os.path.join(ROOT_DIR, 'addons/official/view/web/official_product/service_app.html'),
|
||||
'service_agent': os.path.join(ROOT_DIR, 'addons/official/view/web/official_product/service_agent.html'),
|
||||
'product_page': os.path.join(ROOT_DIR, 'addons/official/view/web/official_product/product_page.html'),
|
||||
'cases_page': os.path.join(ROOT_DIR, 'addons/official/view/web/official_case_show/list.html'),
|
||||
'cases_e': os.path.join(ROOT_DIR, 'addons/official/view/web/official_case_show/list_e.html'),
|
||||
'about_page': os.path.join(ROOT_DIR, 'addons/official/view/web/official_home/single.html'),
|
||||
'about_e': os.path.join(ROOT_DIR, 'addons/official/view/web/official_home/single_e.html'),
|
||||
'contact_page': os.path.join(ROOT_DIR, 'addons/official/view/web/official_online_message/list.html'),
|
||||
'contact_e': os.path.join(ROOT_DIR, 'addons/official/view/web/official_online_message/list_e.html'),
|
||||
'nav_pc': os.path.join(ROOT_DIR, 'addons/official/view/web/official_common/pc/nav.html'),
|
||||
'footer_pc': os.path.join(ROOT_DIR, 'addons/official/view/web/official_common/pc/footer.html'),
|
||||
"index": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_home/index.html"
|
||||
),
|
||||
"index_e": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_home/index_e.html"
|
||||
),
|
||||
"service_page": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_product/service_page.html"
|
||||
),
|
||||
"service_software": os.path.join(
|
||||
ROOT_DIR,
|
||||
"addons/official/view/web/official_product/service_software.html",
|
||||
),
|
||||
"service_app": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_product/service_app.html"
|
||||
),
|
||||
"service_agent": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_product/service_agent.html"
|
||||
),
|
||||
"product_page": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_product/product_page.html"
|
||||
),
|
||||
"cases_page": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_case_show/list.html"
|
||||
),
|
||||
"cases_e": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_case_show/list_e.html"
|
||||
),
|
||||
"about_page": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_home/single.html"
|
||||
),
|
||||
"about_e": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_home/single_e.html"
|
||||
),
|
||||
"contact_page": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_online_message/list.html"
|
||||
),
|
||||
"contact_e": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_online_message/list_e.html"
|
||||
),
|
||||
"nav_pc": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_common/pc/nav.html"
|
||||
),
|
||||
"footer_pc": os.path.join(
|
||||
ROOT_DIR, "addons/official/view/web/official_common/pc/footer.html"
|
||||
),
|
||||
}
|
||||
|
||||
self.contents = {}
|
||||
for key, path in self.files.items():
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
self.contents[key] = f.read()
|
||||
else:
|
||||
self.contents[key] = ''
|
||||
self.contents[key] = ""
|
||||
|
||||
self.all_text = "\n".join(self.contents.values())
|
||||
|
||||
def test_forbid_ai_jargon_and_cliches(self):
|
||||
"""严格杜绝 AI/互联网黑话与套话口癖"""
|
||||
forbidden_jargon = [
|
||||
'闭环', # 典型大厂/AI黑话(如“业务闭环”、“闭环协同”、“交付闭环”)
|
||||
'工匠精神打磨', # 空洞煽情修饰
|
||||
'准备开启您的', # 典型 AI 祈使反问句
|
||||
'矩阵赋能', # AI 常用生造词
|
||||
'双向幂等校验', # 过于晦涩的技术术语,应通俗化为防重复扣款/防重复提交
|
||||
'多路向量重排召回',# 技术晦涩,应通俗化说明知识库检索精准
|
||||
'多路向量召回', # 算法黑话
|
||||
'无感幂等同步', # 技术行话,应通俗化为自动同步防重复
|
||||
'数字化落地样本', # 典型公关大词,客户需要的是业务系统而非“样本”
|
||||
'确定性数字化样本',
|
||||
'知识管线', # 算法黑话,应通俗化为企业知识库
|
||||
'语料处理管线', # 算法行话,应通俗化为知识库处理脚本
|
||||
'操作链路', # UX大厂黑话,应通俗化为操作步骤
|
||||
'脱敏数据沙箱', # 过于晦涩,应通俗化为演示环境
|
||||
'厘清业务痛点', # 居高临下大厂套话,应为调研实际业务流程
|
||||
'全链路打通', # 大厂黑话,应为全面打通或全面贯通
|
||||
'全链路压测', # 应为高并发压力测试
|
||||
"闭环", # 典型大厂/AI黑话(如“业务闭环”、“闭环协同”、“交付闭环”)
|
||||
"工匠精神打磨", # 空洞煽情修饰
|
||||
"准备开启您的", # 典型 AI 祈使反问句
|
||||
"矩阵赋能", # AI 常用生造词
|
||||
"双向幂等校验", # 过于晦涩的技术术语,应通俗化为防重复扣款/防重复提交
|
||||
"多路向量重排召回", # 技术晦涩,应通俗化说明知识库检索精准
|
||||
"多路向量召回", # 算法黑话
|
||||
"无感幂等同步", # 技术行话,应通俗化为自动同步防重复
|
||||
"数字化落地样本", # 典型公关大词,客户需要的是业务系统而非“样本”
|
||||
"确定性数字化样本",
|
||||
"知识管线", # 算法黑话,应通俗化为企业知识库
|
||||
"语料处理管线", # 算法行话,应通俗化为知识库处理脚本
|
||||
"操作链路", # UX大厂黑话,应通俗化为操作步骤
|
||||
"脱敏数据沙箱", # 过于晦涩,应通俗化为演示环境
|
||||
"厘清业务痛点", # 居高临下大厂套话,应为调研实际业务流程
|
||||
"全链路打通", # 大厂黑话,应为全面打通或全面贯通
|
||||
"全链路压测", # 应为高并发压力测试
|
||||
]
|
||||
for jargon in forbidden_jargon:
|
||||
self.assertNotIn(jargon, self.all_text, f"全站文案仍包含黑话或AI套话: '{jargon}'")
|
||||
|
||||
|
||||
self.assertNotIn(
|
||||
jargon, self.all_text, f"全站文案仍包含黑话或AI套话: '{jargon}'"
|
||||
)
|
||||
|
||||
def test_customer_friendly_plain_language(self):
|
||||
"""面向客户与访客的大白话表达:突出价值与保障"""
|
||||
# 首页:通俗的防重复扣款与数据安全
|
||||
self.assertIn('防重复扣款', self.contents['index'], "首页产品指标应使用客户一目了然的'防重复扣款'")
|
||||
self.assertIn(
|
||||
"防重复扣款",
|
||||
self.contents["index"],
|
||||
"首页产品指标应使用客户一目了然的'防重复扣款'",
|
||||
)
|
||||
|
||||
# 智能体页:通俗解释
|
||||
self.assertTrue(
|
||||
'完全离线运行' in self.contents['service_agent'] or '私有化部署' in self.contents['service_agent'],
|
||||
"智能体页必须用通俗大白话说明部署方式与数据边界"
|
||||
"完全离线运行" in self.contents["service_agent"]
|
||||
or "私有化部署" in self.contents["service_agent"],
|
||||
"智能体页必须用通俗大白话说明部署方式与数据边界",
|
||||
)
|
||||
self.assertTrue(
|
||||
any(label in self.contents['service_agent'] for label in ('引用来源', '引用溯源', '引用展示')),
|
||||
"智能体页必须讲清楚回答带有引用来源"
|
||||
any(
|
||||
label in self.contents["service_agent"]
|
||||
for label in ("引用来源", "引用溯源", "引用展示")
|
||||
),
|
||||
"智能体页必须讲清楚回答带有引用来源",
|
||||
)
|
||||
|
||||
# 案例页:拒绝“样本”,使用“数字化系统”;使用“自动同步防重复”
|
||||
self.assertIn('数字化系统', self.contents['cases_page'])
|
||||
self.assertIn('自动同步防重复', self.contents['cases_page'])
|
||||
self.assertNotIn('多路向量召回', self.contents['cases_page'])
|
||||
self.assertIn("数字化系统", self.contents["cases_page"])
|
||||
self.assertIn("自动同步防重复", self.contents["cases_page"])
|
||||
self.assertNotIn("多路向量召回", self.contents["cases_page"])
|
||||
|
||||
# 联系我们表单:使用通俗的“业务需求”而非大厂“痛点”
|
||||
self.assertNotIn('系统痛点', self.contents['contact_page'])
|
||||
self.assertIn('业务需求', self.contents['contact_page'])
|
||||
self.assertNotIn("系统痛点", self.contents["contact_page"])
|
||||
self.assertIn("业务需求", self.contents["contact_page"])
|
||||
|
||||
# 软件开发页:面向客户的直白 CTA
|
||||
self.assertNotIn('准备开启您的企业级定制软件工程?', self.contents['service_software'])
|
||||
self.assertNotIn(
|
||||
"准备开启您的企业级定制软件工程?", self.contents["service_software"]
|
||||
)
|
||||
|
||||
# APP与小程序页:通俗表述
|
||||
self.assertNotIn('私域闭环', self.contents['service_app'])
|
||||
self.assertNotIn("私域闭环", self.contents["service_app"])
|
||||
|
||||
# 产品页:开箱即用与源码买断清晰明了
|
||||
self.assertIn('成熟商业系统', self.contents['product_page'])
|
||||
self.assertIn("成熟商业系统", self.contents["product_page"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -9,134 +9,251 @@
|
||||
5. CSS 针对配图有严格的响应式(max-width: 100%、圆角、细线边框、悬停微光)控制
|
||||
6. 无破损路径或 404 资源引用
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
class SiteImagesTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.img_dir = os.path.join(BASE_DIR, 'public', 'assets', 'addons', 'official', 'images', 'showcases')
|
||||
self.pc_home_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_home', 'index.html')
|
||||
self.mobile_home_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_home', 'index_e.html')
|
||||
self.service_page_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_product', 'service_page.html')
|
||||
self.product_page_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_product', 'product_page.html')
|
||||
self.case_page_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_case_show', 'list.html')
|
||||
self.case_mobile_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_case_show', 'list_e.html')
|
||||
self.about_page_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_home', 'single.html')
|
||||
self.about_mobile_path = os.path.join(BASE_DIR, 'addons', 'official', 'view', 'web', 'official_home', 'single_e.html')
|
||||
self.css_path = os.path.join(BASE_DIR, 'public', 'assets', 'addons', 'official', 'css', 'modern_nhc.css')
|
||||
self.img_dir = os.path.join(
|
||||
BASE_DIR, "public", "assets", "addons", "official", "images", "showcases"
|
||||
)
|
||||
self.pc_home_path = os.path.join(
|
||||
BASE_DIR, "addons", "official", "view", "web", "official_home", "index.html"
|
||||
)
|
||||
self.mobile_home_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_home",
|
||||
"index_e.html",
|
||||
)
|
||||
self.service_page_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_product",
|
||||
"service_page.html",
|
||||
)
|
||||
self.product_page_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_product",
|
||||
"product_page.html",
|
||||
)
|
||||
self.case_page_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_case_show",
|
||||
"list.html",
|
||||
)
|
||||
self.case_mobile_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_case_show",
|
||||
"list_e.html",
|
||||
)
|
||||
self.about_page_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_home",
|
||||
"single.html",
|
||||
)
|
||||
self.about_mobile_path = os.path.join(
|
||||
BASE_DIR,
|
||||
"addons",
|
||||
"official",
|
||||
"view",
|
||||
"web",
|
||||
"official_home",
|
||||
"single_e.html",
|
||||
)
|
||||
self.css_path = os.path.join(
|
||||
BASE_DIR, "public", "assets", "addons", "official", "css", "modern_nhc.css"
|
||||
)
|
||||
|
||||
def test_01_image_assets_exist_and_non_empty(self):
|
||||
"""核心配图资源必须物理存在且内容非空"""
|
||||
required_images = [
|
||||
# 服务类配图
|
||||
'service_middle.svg',
|
||||
'service_app.svg',
|
||||
'service_agent.svg',
|
||||
"service_middle.svg",
|
||||
"service_app.svg",
|
||||
"service_agent.svg",
|
||||
# 产品类配图
|
||||
'product_mall.svg',
|
||||
'product_pos.svg',
|
||||
'product_erp.svg',
|
||||
'product_saas.svg',
|
||||
"product_mall.svg",
|
||||
"product_pos.svg",
|
||||
"product_erp.svg",
|
||||
"product_saas.svg",
|
||||
# 案例类配图
|
||||
'case_distrib.svg',
|
||||
'case_catering.svg',
|
||||
'case_gov_agent.svg',
|
||||
'case_manufacture.svg',
|
||||
"case_distrib.svg",
|
||||
"case_catering.svg",
|
||||
"case_gov_agent.svg",
|
||||
"case_manufacture.svg",
|
||||
# 专家与资质徽标
|
||||
'team_architect.svg',
|
||||
'team_fullstack.svg',
|
||||
'team_ai.svg',
|
||||
'team_security.svg',
|
||||
'cert_hi_tech.svg',
|
||||
'cert_iso9001.svg'
|
||||
"team_architect.svg",
|
||||
"team_fullstack.svg",
|
||||
"team_ai.svg",
|
||||
"team_security.svg",
|
||||
"cert_hi_tech.svg",
|
||||
"cert_iso9001.svg",
|
||||
]
|
||||
self.assertTrue(os.path.isdir(self.img_dir), f"图片目录 {self.img_dir} 必须存在")
|
||||
self.assertTrue(
|
||||
os.path.isdir(self.img_dir), f"图片目录 {self.img_dir} 必须存在"
|
||||
)
|
||||
for img_name in required_images:
|
||||
img_path = os.path.join(self.img_dir, img_name)
|
||||
self.assertTrue(os.path.isfile(img_path), f"图片文件 {img_name} 必须存在于 showcases 目录下")
|
||||
self.assertTrue(
|
||||
os.path.isfile(img_path),
|
||||
f"图片文件 {img_name} 必须存在于 showcases 目录下",
|
||||
)
|
||||
size = os.path.getsize(img_path)
|
||||
self.assertGreater(size, 100, f"图片文件 {img_name} 大小不能过小")
|
||||
|
||||
def test_02_pc_and_mobile_homepage_service_images(self):
|
||||
"""首页服务入口必须包含三个一致的服务视觉卡片"""
|
||||
with open(self.pc_home_path, encoding='utf-8') as f:
|
||||
with open(self.pc_home_path, encoding="utf-8") as f:
|
||||
pc_content = f.read()
|
||||
with open(self.mobile_home_path, encoding='utf-8') as f:
|
||||
with open(self.mobile_home_path, encoding="utf-8") as f:
|
||||
mobile_content = f.read()
|
||||
|
||||
for content, page_name in [(pc_content, "PC 首页"), (mobile_content, "移动端首页")]:
|
||||
self.assertEqual(content.count('hn-service-top'), 3, f"{page_name} 应包含三个服务视觉卡片")
|
||||
for service_name in ['软件开发', 'APP与小程序定制', '智能体开发']:
|
||||
self.assertIn(service_name, content, f"{page_name} 应包含服务入口:{service_name}")
|
||||
for content, page_name in [
|
||||
(pc_content, "PC 首页"),
|
||||
(mobile_content, "移动端首页"),
|
||||
]:
|
||||
self.assertEqual(
|
||||
content.count("hn-service-top"),
|
||||
3,
|
||||
f"{page_name} 应包含三个服务视觉卡片",
|
||||
)
|
||||
for service_name in ["软件开发", "APP与小程序定制", "智能体开发"]:
|
||||
self.assertIn(
|
||||
service_name, content, f"{page_name} 应包含服务入口:{service_name}"
|
||||
)
|
||||
|
||||
def test_03_pc_and_mobile_homepage_product_images(self):
|
||||
"""首页产品区必须包含经营工作台示意与四类产品入口"""
|
||||
with open(self.pc_home_path, encoding='utf-8') as f:
|
||||
with open(self.pc_home_path, encoding="utf-8") as f:
|
||||
pc_content = f.read()
|
||||
with open(self.mobile_home_path, encoding='utf-8') as f:
|
||||
with open(self.mobile_home_path, encoding="utf-8") as f:
|
||||
mobile_content = f.read()
|
||||
|
||||
for content, page_name in [(pc_content, "PC 首页"), (mobile_content, "移动端首页")]:
|
||||
self.assertIn('hn-app-window', content, f"{page_name} 应包含经营工作台界面示意")
|
||||
self.assertIn('hn-chart', content, f"{page_name} 应包含经营趋势示意")
|
||||
for product_name in ['商城', '收银', '进销存', 'SaaS版本']:
|
||||
self.assertIn(product_name, content, f"{page_name} 应包含产品入口:{product_name}")
|
||||
for content, page_name in [
|
||||
(pc_content, "PC 首页"),
|
||||
(mobile_content, "移动端首页"),
|
||||
]:
|
||||
self.assertIn(
|
||||
"hn-app-window", content, f"{page_name} 应包含经营工作台界面示意"
|
||||
)
|
||||
self.assertIn("hn-chart", content, f"{page_name} 应包含经营趋势示意")
|
||||
for product_name in ["商城", "收银", "进销存", "SaaS版本"]:
|
||||
self.assertIn(
|
||||
product_name, content, f"{page_name} 应包含产品入口:{product_name}"
|
||||
)
|
||||
|
||||
def test_04_cases_pages_have_preview_images(self):
|
||||
"""标杆案例卡片必须配有真实的业务系统视窗图"""
|
||||
with open(self.case_page_path, encoding='utf-8') as f:
|
||||
with open(self.case_page_path, encoding="utf-8") as f:
|
||||
case_pc = f.read()
|
||||
with open(self.case_mobile_path, encoding='utf-8') as f:
|
||||
with open(self.case_mobile_path, encoding="utf-8") as f:
|
||||
case_mobile = f.read()
|
||||
|
||||
for content, page_name in [(case_pc, "PC 案例独立页"), (case_mobile, "移动端案例独立页")]:
|
||||
self.assertIn('case_distrib.svg', content, f"{page_name} 应包含分销订货中台界面图")
|
||||
self.assertIn('case_catering.svg', content, f"{page_name} 应包含连锁餐饮收银界面图")
|
||||
self.assertIn('case_gov_agent.svg', content, f"{page_name} 应包含政务智能体界面图")
|
||||
self.assertIn('case_manufacture.svg', content, f"{page_name} 应包含工业制造进销存界面图")
|
||||
for content, page_name in [
|
||||
(case_pc, "PC 案例独立页"),
|
||||
(case_mobile, "移动端案例独立页"),
|
||||
]:
|
||||
self.assertIn(
|
||||
"case_distrib.svg", content, f"{page_name} 应包含分销订货中台界面图"
|
||||
)
|
||||
self.assertIn(
|
||||
"case_catering.svg", content, f"{page_name} 应包含连锁餐饮收银界面图"
|
||||
)
|
||||
self.assertIn(
|
||||
"case_gov_agent.svg", content, f"{page_name} 应包含政务智能体界面图"
|
||||
)
|
||||
self.assertIn(
|
||||
"case_manufacture.svg",
|
||||
content,
|
||||
f"{page_name} 应包含工业制造进销存界面图",
|
||||
)
|
||||
|
||||
def test_05_service_and_product_pages_have_showcases(self):
|
||||
"""独立服务页与产品页必须嵌入高质量系统拓扑与界面大图"""
|
||||
with open(self.service_page_path, encoding='utf-8') as f:
|
||||
with open(self.service_page_path, encoding="utf-8") as f:
|
||||
service_content = f.read()
|
||||
with open(self.product_page_path, encoding='utf-8') as f:
|
||||
with open(self.product_page_path, encoding="utf-8") as f:
|
||||
product_content = f.read()
|
||||
|
||||
self.assertIn('service_middle.svg', service_content)
|
||||
self.assertIn('service_app.svg', service_content)
|
||||
self.assertIn('service_agent.svg', service_content)
|
||||
self.assertIn("service_middle.svg", service_content)
|
||||
self.assertIn("service_app.svg", service_content)
|
||||
self.assertIn("service_agent.svg", service_content)
|
||||
|
||||
self.assertIn('product_mall.svg', product_content)
|
||||
self.assertIn('product_pos.svg', product_content)
|
||||
self.assertIn('product_erp.svg', product_content)
|
||||
self.assertIn('product_saas.svg', product_content)
|
||||
self.assertIn("product_mall.svg", product_content)
|
||||
self.assertIn("product_pos.svg", product_content)
|
||||
self.assertIn("product_erp.svg", product_content)
|
||||
self.assertIn("product_saas.svg", product_content)
|
||||
|
||||
def test_06_about_pages_have_expert_and_cert_badges(self):
|
||||
"""关于我们页必须包含专家团队专业徽标与资质认证图"""
|
||||
with open(self.about_page_path, encoding='utf-8') as f:
|
||||
def test_06_about_pages_have_abstract_brand_images(self):
|
||||
"""关于我们使用已标注的品牌抽象配图,保留格式回退。"""
|
||||
with open(self.about_page_path, encoding="utf-8") as f:
|
||||
about_pc = f.read()
|
||||
with open(self.about_mobile_path, encoding='utf-8') as f:
|
||||
with open(self.about_mobile_path, encoding="utf-8") as f:
|
||||
about_mobile = f.read()
|
||||
|
||||
for content, page_name in [(about_pc, "PC 关于我们页"), (about_mobile, "移动端关于我们页")]:
|
||||
self.assertIn('team_architect.svg', content, f"{page_name} 应包含首席架构师徽章")
|
||||
self.assertIn('team_fullstack.svg', content, f"{page_name} 应包含全栈研发总监徽章")
|
||||
self.assertIn('team_ai.svg', content, f"{page_name} 应包含AI智能体专家徽章")
|
||||
self.assertIn('team_security.svg', content, f"{page_name} 应包含质量安全合规专家徽章")
|
||||
self.assertIn('cert_hi_tech.svg', content, f"{page_name} 应包含高新技术企业认证图")
|
||||
self.assertIn('cert_iso9001.svg', content, f"{page_name} 应包含ISO9001认证图")
|
||||
for content, page_name in [
|
||||
(about_pc, "PC 关于我们页"),
|
||||
(about_mobile, "移动端关于我们页"),
|
||||
]:
|
||||
for name in ("landscape", "collaboration"):
|
||||
for extension in ("webp", "jpg"):
|
||||
relative_path = f"images/about/{name}.{extension}"
|
||||
self.assertIn(
|
||||
relative_path, content, f"{page_name} 应包含抽象配图及格式回退"
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(
|
||||
os.path.join(
|
||||
BASE_DIR,
|
||||
"public",
|
||||
"assets",
|
||||
"addons",
|
||||
"official",
|
||||
relative_path,
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertIn("抽象建筑示意", content)
|
||||
self.assertNotIn("cert_hi_tech.svg", content)
|
||||
self.assertNotIn("cert_iso9001.svg", content)
|
||||
|
||||
def test_07_responsive_image_styles_in_css(self):
|
||||
"""CSS 必须具备严谨的配图自适应与质感样式定义"""
|
||||
with open(self.css_path, encoding='utf-8') as f:
|
||||
with open(self.css_path, encoding="utf-8") as f:
|
||||
css = f.read()
|
||||
|
||||
self.assertIn('.nhc-card-thumb', css, "CSS 应定义 .nhc-card-thumb 图片容器类")
|
||||
self.assertIn('object-fit', css, "CSS 应规范图片的适应展示模式")
|
||||
self.assertIn('.nhc-case-img-wrap', css, "CSS 应定义案例图视窗包装器")
|
||||
self.assertIn('.nhc-service-media-box', css, "CSS 应定义服务媒体容器")
|
||||
self.assertIn(".nhc-card-thumb", css, "CSS 应定义 .nhc-card-thumb 图片容器类")
|
||||
self.assertIn("object-fit", css, "CSS 应规范图片的适应展示模式")
|
||||
self.assertIn(".nhc-case-img-wrap", css, "CSS 应定义案例图视窗包装器")
|
||||
self.assertIn(".nhc-service-media-box", css, "CSS 应定义服务媒体容器")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
测试首页高端主视觉系统 (Test Premium Home Hero Visual)
|
||||
验证宽幅首屏摄影的高清画质、响应式图片源、桌面与移动端一致引用、
|
||||
无文字遮挡的渐变处理、响应式断点与减少动态效果适配。
|
||||
验证宽幅首屏摄影的高清画质、桌面与移动端响应式图片源、
|
||||
两端模板一致引用、优先加载与有效替代文本、
|
||||
渐变处理、响应式断点与减少动态效果适配。
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -10,25 +11,39 @@ import unittest
|
||||
from PIL import Image
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
HERO_IMAGE_DIR = os.path.join(ROOT_DIR, "public", "assets", "addons", "official", "images", "home")
|
||||
CSS_PATH = os.path.join(ROOT_DIR, "public", "assets", "addons", "official", "css", "home-v3.css")
|
||||
PC_HOME_PATH = os.path.join(ROOT_DIR, "addons", "official", "view", "web", "official_home", "index.html")
|
||||
M_HOME_PATH = os.path.join(ROOT_DIR, "addons", "official", "view", "web", "official_home", "index_e.html")
|
||||
HERO_IMAGE_DIR = os.path.join(
|
||||
ROOT_DIR, "public", "assets", "addons", "official", "images", "home"
|
||||
)
|
||||
CSS_PATH = os.path.join(
|
||||
ROOT_DIR, "public", "assets", "addons", "official", "css", "home-v3.css"
|
||||
)
|
||||
PC_HOME_PATH = os.path.join(
|
||||
ROOT_DIR, "addons", "official", "view", "web", "official_home", "index.html"
|
||||
)
|
||||
M_HOME_PATH = os.path.join(
|
||||
ROOT_DIR, "addons", "official", "view", "web", "official_home", "index_e.html"
|
||||
)
|
||||
|
||||
|
||||
class PremiumHeroBannerTest(unittest.TestCase):
|
||||
"""验证首页宽幅主视觉"""
|
||||
|
||||
def test_01_hero_image_high_quality(self):
|
||||
"""1. 验证首屏宽幅摄影物理存在、分辨率高、画质饱满"""
|
||||
hero_path = os.path.join(HERO_IMAGE_DIR, "river-courtyard-v3.jpg")
|
||||
self.assertTrue(os.path.exists(hero_path), "首屏宽幅摄影不存在")
|
||||
self.assertGreater(os.path.getsize(hero_path), 150 * 1024, "首屏摄影文件过小")
|
||||
"""1. 验证桌面与移动端首屏摄影物理存在、分辨率高、画质饱满"""
|
||||
hero_images = {
|
||||
"ai-computing-v4.jpg": (1983, 793),
|
||||
"ai-computing-mobile-v4.jpg": (1024, 1536),
|
||||
}
|
||||
for filename, (minimum_width, minimum_height) in hero_images.items():
|
||||
with self.subTest(hero=filename):
|
||||
hero_path = os.path.join(HERO_IMAGE_DIR, filename)
|
||||
self.assertTrue(os.path.exists(hero_path), f"首屏宽幅摄影不存在: {filename}")
|
||||
self.assertGreater(os.path.getsize(hero_path), 100 * 1024, f"首屏摄影文件过小: {filename}")
|
||||
|
||||
with Image.open(hero_path) as hero:
|
||||
width, height = hero.size
|
||||
self.assertGreaterEqual(width, 1800, "首屏摄影宽度不足宽幅要求")
|
||||
self.assertGreaterEqual(height, 700, "首屏摄影高度不足首屏要求")
|
||||
with Image.open(hero_path) as hero:
|
||||
width, height = hero.size
|
||||
self.assertGreaterEqual(width, minimum_width, f"首屏摄影宽度不足要求: {filename}")
|
||||
self.assertGreaterEqual(height, minimum_height, f"首屏摄影高度不足要求: {filename}")
|
||||
|
||||
def test_02_css_uses_responsive_hero_and_accessible_motion(self):
|
||||
"""2. 验证专属 CSS 提供首屏布局、响应式断点与减少动态效果适配"""
|
||||
@@ -42,15 +57,24 @@ class PremiumHeroBannerTest(unittest.TestCase):
|
||||
self.assertIn("prefers-reduced-motion", css_content, "CSS 应支持减少动态效果")
|
||||
|
||||
def test_03_pc_and_mobile_template_integrity(self):
|
||||
"""3. 验证 PC 与移动端模板引用同一套响应式首屏图片"""
|
||||
"""3. 验证 PC 与移动端模板引用桌面与移动端两套响应式首屏图片"""
|
||||
for page_path in [PC_HOME_PATH, M_HOME_PATH]:
|
||||
with open(page_path, encoding="utf-8") as stream:
|
||||
html = stream.read()
|
||||
self.assertEqual(html.count("hn-hero-picture"), 1, f"首屏图片容器数量应为 1:{page_path}")
|
||||
self.assertIn("river-courtyard-v3.webp", html, f"页面缺少 WebP 首屏图片源:{page_path}")
|
||||
self.assertIn("river-courtyard-v3.jpg", html, f"页面缺少 JPEG 首屏图片回退:{page_path}")
|
||||
for filename in (
|
||||
"ai-computing-v4.webp",
|
||||
"ai-computing-v4.jpg",
|
||||
"ai-computing-mobile-v4.webp",
|
||||
"ai-computing-mobile-v4.jpg",
|
||||
):
|
||||
self.assertIn(filename, html, f"页面缺少首屏图片资源 {filename}:{page_path}")
|
||||
self.assertIn('fetchpriority="high"', html, f"首屏图片应优先加载:{page_path}")
|
||||
self.assertRegex(html, r'<img[^>]+alt="[^"]{4,}"', f"首屏图片应包含有效替代文本:{page_path}")
|
||||
self.assertRegex(
|
||||
html,
|
||||
r'<img[^>]+alt="[^"]{4,}"',
|
||||
f"首屏图片应包含有效替代文本:{page_path}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
在新工单中引用
屏蔽一个用户