639 lines
41 KiB
JavaScript
639 lines
41 KiB
JavaScript
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
|
||
const workspace = process.cwd()
|
||
const failures = []
|
||
const read = (filePath) => fs.readFileSync(path.join(workspace, filePath), 'utf8')
|
||
const expect = (condition, message) => {
|
||
if (!condition) failures.push(message)
|
||
}
|
||
const listPageSources = (directory = 'pages') => fs.readdirSync(path.join(workspace, directory), { withFileTypes: true })
|
||
.flatMap((entry) => {
|
||
const childPath = path.join(directory, entry.name)
|
||
return entry.isDirectory()
|
||
? listPageSources(childPath)
|
||
: entry.isFile() && entry.name.endsWith('.vue') ? [childPath] : []
|
||
})
|
||
|
||
const signIn = read('pages/auth/sign-in.vue')
|
||
const loginAdvertisement = read('components/auth/LoginAdvertisement.vue')
|
||
const register = read('pages/auth/register.vue')
|
||
const routes = read('utils/navigation/routes.js')
|
||
const genealogyHome = read('pages/genealogy/my-genealogies.vue')
|
||
const profileHomePage = read('pages/profile/home.vue')
|
||
const homeAdvertisementPanel = read('components/genealogy/HomeAdvertisementPanel.vue')
|
||
const genealogyOverview = read('pages/genealogy/overview.vue')
|
||
const genealogyCard = read('components/genealogy/Card.vue')
|
||
const familyFeedPage = read('pages/family/feed.vue')
|
||
const siteHomePage = read('pages/family/site-home.vue')
|
||
const siteContentContract = read('services/api/site-content-contract.js')
|
||
const invitationManager = read('components/genealogy/InvitationManager.vue')
|
||
const pedigree = read('pages/tree/pedigree.vue')
|
||
const treeOverview = read('pages/tree/overview.vue')
|
||
const moduleBackground = read('components/ModulePageBackground.vue')
|
||
const genealogyBackground = read('components/genealogy/PageBackground.vue')
|
||
const vipPage = read('pages/profile/vip.vue')
|
||
const promotionsPage = read('pages/profile/promotions.vue')
|
||
const referralQrCode = read('components/ReferralQrCode.vue')
|
||
const helpPage = read('pages/profile/help.vue')
|
||
const editProfilePage = read('pages/profile/edit-profile.vue')
|
||
const genealogySettingsPage = read('pages/genealogy/settings.vue')
|
||
const genealogyCreatePage = read('pages/genealogy/create.vue')
|
||
const familyVideosPage = read('pages/family/videos.vue')
|
||
const articleEditorPage = read('pages/family/article-editor.vue')
|
||
const ceremonyEditorPage = read('pages/records/ceremony-editor.vue')
|
||
const ceremonyListPage = read('pages/records/ceremonies.vue')
|
||
const articleListPage = read('pages/family/articles.vue')
|
||
const personDocumentsPage = read('pages/records/person-documents.vue')
|
||
const personDocumentDialog = read('components/tree/PersonDocumentDialog.vue')
|
||
const earningsPage = read('pages/profile/earnings.vue')
|
||
const relativeRecordsPage = read('pages/records/relative-records.vue')
|
||
const growthJournalPage = read('pages/records/growth-journal.vue')
|
||
const messageCenterPage = read('pages/notification/message-center.vue')
|
||
const changePhonePage = read('pages/profile/change-phone.vue')
|
||
const changePasswordPage = read('pages/profile/change-password.vue')
|
||
const platformVideosPage = read('pages/family/platform-videos.vue')
|
||
const familyMediaContract = read('services/api/family-media-contract.js')
|
||
const batchDeletionComposable = read('composables/use-batch-deletion.js')
|
||
const batchManagementBar = read('components/BatchManagementBar.vue')
|
||
const batchSelectionMark = read('components/BatchSelectionMark.vue')
|
||
const familyArticleService = read('services/api/family-article-service.js')
|
||
const familyArticleContract = read('services/api/family-article-contract.js')
|
||
const lifeRecordService = read('services/api/life-record-service.js')
|
||
const dialogPages = [
|
||
'pages/tree/pedigree.vue',
|
||
'pages/family/album-detail.vue',
|
||
'pages/records/relative-records.vue',
|
||
'pages/genealogy/search.vue',
|
||
'pages/family/article-detail.vue',
|
||
'pages/notification/message-detail.vue',
|
||
'pages/notification/message-center.vue',
|
||
'pages/profile/earnings.vue',
|
||
]
|
||
const vipService = read('services/api/vip-service.js')
|
||
const siteContentService = read('services/api/site-content-service.js')
|
||
const requestClient = read('services/api/request-client.js')
|
||
const requestErrorMessage = read('services/api/request-error-message.js')
|
||
const memberDirectoryPage = read('pages/tree/member-directory.vue')
|
||
const peoplePage = read('pages/records/people.vue')
|
||
const idValidatedPages = [
|
||
'pages/tree/add-relative.vue',
|
||
'pages/tree/member-profile.vue',
|
||
'pages/tree/member-states.vue',
|
||
'pages/genealogy/generation-poems.vue',
|
||
'pages/genealogy/overview.vue',
|
||
'pages/records/person-detail.vue',
|
||
]
|
||
const memberRankPage = read('pages/tree/member-rank.vue')
|
||
const meritRecordsPage = read('pages/records/merit-records.vue')
|
||
const memoPage = read('pages/records/memos.vue')
|
||
const lifeRecordContract = read('services/api/life-record-contract.js')
|
||
const ceremonyDetailPage = read('pages/records/ceremony-detail.vue')
|
||
const relativeRecordEditorPage = read('pages/records/relative-record-editor.vue')
|
||
const requestNormalizers = read('services/api/request-normalizers.js')
|
||
const profileContract = read('services/api/profile-contract.js')
|
||
const genealogyContext = read('utils/genealogy/context.js')
|
||
const editMemberPage = read('pages/tree/edit-member.vue')
|
||
const addRelativePage = read('pages/tree/add-relative.vue')
|
||
const securityPage = read('pages/profile/security.vue')
|
||
const lineageService = read('services/api/lineage-service.js')
|
||
const lineageWriteContract = read('services/api/lineage-write-contract.js')
|
||
const lineagePersonContract = read('services/api/lineage-person-contract.js')
|
||
const feedDetailPage = read('pages/family/feed-detail.vue')
|
||
const articleDetailPage = read('pages/family/article-detail.vue')
|
||
const familyVideosPageSource = read('pages/family/videos.vue')
|
||
const genealogyContract = read('services/api/genealogy-contract.js')
|
||
const genealogySearchPage = read('pages/genealogy/search.vue')
|
||
const generationPoemService = read('services/api/generation-poem-service.js')
|
||
const genealogyMemberService = read('services/api/genealogy-member-service.js')
|
||
const personDocumentService = read('services/api/person-document-service.js')
|
||
const mediaUpload = read('utils/media-upload.js')
|
||
const businessFileContract = read('services/api/business-file-contract.js')
|
||
const authContract = read('services/api/auth-contract.js')
|
||
const authService = read('services/api/auth-service.js')
|
||
const contentRecoveryContract = read('services/api/content-password-recovery-contract.js')
|
||
const contentRecoveryService = read('services/api/content-password-recovery-service.js')
|
||
const referralContract = read('services/api/referral-contract.js')
|
||
const permissionContract = read('services/api/genealogy-permission-contract.js')
|
||
const genealogyCapabilityService = read('services/api/genealogy-capability-service.js')
|
||
const businessDictionaryContract = read('services/api/business-dictionary-contract.js')
|
||
const vipContract = read('services/api/vip-contract.js')
|
||
const tacLibrary = read('static/tac/js/tac.min.js')
|
||
const openApi = read('genealogy-app-openapi.yaml')
|
||
const manifest = JSON.parse(read('manifest.json'))
|
||
|
||
for (const pagePath of listPageSources()) {
|
||
expect(!/暂不支持|开发中|敬请期待|准备中/.test(read(pagePath)), `${pagePath} 仍展示占位功能提示`)
|
||
}
|
||
|
||
expect(manifest.name === '代代相传家谱', 'manifest 应用名称与正式产品名称不一致')
|
||
|
||
expect(!signIn.includes('协议页面准备中'), '登录页仍使用协议占位提示')
|
||
expect(!register.includes('协议页面准备中'), '注册页仍使用协议占位提示')
|
||
expect(signIn.includes('openComplianceDocument'), '登录页没有接入协议正文导航')
|
||
expect(register.includes('openComplianceDocument'), '注册页没有接入协议正文导航')
|
||
expect(
|
||
signIn.includes('<LoginAdvertisement') &&
|
||
loginAdvertisement.includes('placement: "home_banner"') &&
|
||
loginAdvertisement.includes('promotion.coverFile.accessUrl') &&
|
||
loginAdvertisement.includes('home-ad-heritage-hall.png') &&
|
||
!loginAdvertisement.includes('点击查看更多内容'),
|
||
'登录页没有在注册入口上方显示单张祠堂山水图,或仍带有查看更多文案',
|
||
)
|
||
expect(
|
||
signIn.includes('justify-content: flex-start') &&
|
||
signIn.includes('color: #5d493b') &&
|
||
signIn.includes('color: #766c63'),
|
||
'登录页仍使用自动撑开的旧布局,或次要文字颜色没有加深',
|
||
)
|
||
expect(
|
||
/\.login-page\s*\{[^}]*height:\s*var\(--app-viewport-height\)[^}]*max-height:\s*var\(--app-viewport-height\)[^}]*overflow:\s*hidden/.test(signIn) &&
|
||
/\.login-page\s+:deep\(\.auth-shell__paper\)\s*\{[^}]*min-height:\s*0[^}]*overflow:\s*hidden/.test(signIn),
|
||
'登录页没有锁定动态 100vh,新增广告后仍可能产生页面级滚动',
|
||
)
|
||
expect(
|
||
/getPromotions[\s\S]*?authenticated:\s*false/.test(siteContentService),
|
||
'公开广告接口仍会携带登录态,登录页无法安全读取 home_banner',
|
||
)
|
||
expect(
|
||
/getComplianceDocument[\s\S]*?authenticated:\s*false/.test(siteContentService),
|
||
'协议正文请求仍依赖登录状态',
|
||
)
|
||
|
||
const complianceRoute = routes.match(/M13: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
|
||
expect(complianceRoute.includes('"A01"'), '协议正文路由不允许从登录页进入')
|
||
expect(complianceRoute.includes('"A04"'), '协议正文路由不允许从注册页进入')
|
||
const membersRoute = routes.match(/G13: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
|
||
expect(!membersRoute.includes('"G01"'), '成员页仍允许从家谱首页旧入口进入')
|
||
expect(
|
||
genealogyOverview.includes('openPage("G13"') &&
|
||
!genealogyHome.includes('shortcut-grid') &&
|
||
!genealogyHome.includes('openShortcut'),
|
||
'家谱快捷导航没有完整迁移到家谱详情页',
|
||
)
|
||
const platformVideosRoute = routes.match(/F11: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
|
||
expect(!platformVideosRoute.includes('"G01"') && platformVideosRoute.includes('"F10"'), '宣传视频没有迁回原视频导航')
|
||
expect(platformVideosRoute.includes('"videoId"'), '宣传视频路由不能携带指定视频 ID')
|
||
expect(
|
||
familyMediaContract.includes("HOME_FEATURED: 'home_featured'") &&
|
||
familyMediaContract.includes("VIDEO_CENTER: 'video_center'") &&
|
||
familyMediaContract.includes("PROFILE_FEATURED: 'profile_featured'"),
|
||
'平台视频契约没有完整覆盖后端三个投放位',
|
||
)
|
||
expect(
|
||
!genealogyHome.includes('featured-media-grid') &&
|
||
!genealogyHome.includes('PLATFORM_VIDEO_PLACEMENT.HOME_FEATURED'),
|
||
'家谱首页仍保留已经迁出的宣传视频',
|
||
)
|
||
expect(
|
||
!genealogyHome.includes('genealogy-create-button') &&
|
||
!genealogyHome.includes('新建家谱') &&
|
||
genealogyHome.includes('class="create-action"') &&
|
||
genealogyHome.includes('添加家谱'),
|
||
'家谱首页仍在顶部显示误加的新建按钮,或列表末尾缺少原有添加入口',
|
||
)
|
||
expect(
|
||
genealogyHome.indexOf('class="genealogy-list-scroll"') <
|
||
genealogyHome.indexOf('class="genealogy-list-toolbar"') &&
|
||
!genealogyHome.includes('genealogy-fixed-zone'),
|
||
'家谱分组标题仍被固定在第 7 条标注的滚动区域之外',
|
||
)
|
||
expect(
|
||
siteContentContract.includes("news: '资讯'") &&
|
||
siteContentContract.includes("notice: '公告'") &&
|
||
siteHomePage.includes('article.typeLabel'),
|
||
'官网文章类型仍直接暴露后端英文编码',
|
||
)
|
||
expect(
|
||
genealogyHome.includes('<HomeAdvertisementPanel') &&
|
||
homeAdvertisementPanel.includes('home-ad-heritage-hall.png') &&
|
||
homeAdvertisementPanel.includes('home-ad-family-tree.png') &&
|
||
homeAdvertisementPanel.includes('点击查看更多内容') &&
|
||
!genealogyHome.includes('home-news-card') &&
|
||
!genealogyHome.includes('<AppPromotionStrip'),
|
||
'首页底部固定区域没有按参考图显示两张并排图片,或仍使用错误的文字/横向广告卡',
|
||
)
|
||
expect(
|
||
genealogyCard.includes('创建于 {{ formatGenealogyDate(genealogy.createTime) }}') &&
|
||
!genealogyCard.includes('加入于 {{ formatGenealogyDate(genealogy.joinTime) }}'),
|
||
'家谱卡片没有按后端真实 createTime 显示创建日期',
|
||
)
|
||
expect(
|
||
genealogyOverview.includes('familyContentGroups') &&
|
||
genealogyOverview.includes('openFamilySection') &&
|
||
!familyFeedPage.includes('feed-shortcuts'),
|
||
'家族内容导航没有迁入家谱详情并完成归类',
|
||
)
|
||
expect(
|
||
siteHomePage.includes('siteContentApi.getSiteArticles') &&
|
||
siteHomePage.includes('<AppTabbar v-if="!subpageMode" active="family"') &&
|
||
!familyFeedPage.includes('<AppTabbar'),
|
||
'消息根页没有继续承载公共新闻资讯,或家谱动态仍占用根导航',
|
||
)
|
||
expect(read('components/AppTabbar.vue').includes('label: "消息"'), '底部中间导航没有改为消息')
|
||
expect(
|
||
fs.existsSync(path.join(workspace, 'pages/family/site-article-list.vue')) &&
|
||
read('pages/family/site-article-list.vue').includes('subpage-mode'),
|
||
'公共资讯子列表没有与消息根页复用同一实现',
|
||
)
|
||
expect(routes.includes('path: "/pages/family/site-home"') && !routes.includes('path: "/pages/notification/message-home"') && !routes.includes('path: "/pages/profile/message-home"'), '消息根页没有指向原新闻资讯页面')
|
||
expect(!profileHomePage.includes('isMessagePortal'), '我的页面仍混入消息页变体逻辑')
|
||
expect(!profileHomePage.includes('class="profile-metadata"'), '我的页面仍显示应删除的性别、生日和邮箱摘要卡')
|
||
expect(profileHomePage.includes('家谱新闻列表(图文)') && profileHomePage.includes('家谱系列文章(图文)'), '我的菜单没有按标注图补齐')
|
||
expect(
|
||
profileHomePage.includes('<HomeAdvertisementPanel') &&
|
||
profileHomePage.includes('class="profile-home-advertisements"') &&
|
||
!profileHomePage.includes('广告位列表') &&
|
||
!routes.includes('F14: defineRoute'),
|
||
'我的页面底部没有直接复用首页图片广告,或仍保留错误的广告位列表入口',
|
||
)
|
||
expect(
|
||
platformVideosPage.includes('video-card__cover-button') &&
|
||
platformVideosPage.includes('requestedVideoId'),
|
||
'宣传视频列表没有按封面进入指定视频播放',
|
||
)
|
||
expect(
|
||
genealogyHome.includes('genealogyListError') &&
|
||
genealogyHome.includes('getRequestErrorMessage'),
|
||
'家谱首页仍会隐藏家谱列表的真实失败类型',
|
||
)
|
||
expect(
|
||
/import\s*\{[^}]*normalizeOptionalNumericId[^}]*\}\s*from '\.\/response-normalizers\.js'/.test(genealogyContract),
|
||
'家谱响应契约使用 normalizeOptionalNumericId 时没有从响应归一化模块导入',
|
||
)
|
||
expect(
|
||
articleListPage.includes('articleCategoryError') &&
|
||
articleListPage.includes('getRequestErrorMessage'),
|
||
'谱文分类读取失败仍被静默伪装为空分类',
|
||
)
|
||
expect(
|
||
!/loadArticleCategories[\s\S]*?catch\s*\([^)]*\)[\s\S]*?return\s+\[\]/.test(articleListPage),
|
||
'谱文分类读取失败仍直接返回空数组',
|
||
)
|
||
|
||
expect(
|
||
invitationManager.includes('.invitation-manager__row .app-button'),
|
||
'邀请记录按钮没有独立收缩规则',
|
||
)
|
||
expect(
|
||
!/invitation-manager__row text:first-child\s*\{[^}]*overflow-wrap:\s*anywhere/.test(invitationManager),
|
||
'邀请记录名称仍允许逐字断行',
|
||
)
|
||
|
||
expect(pedigree.includes('isVerticalPedigreeText'), '世系表格没有区分中英文排版')
|
||
expect(pedigree.includes('member-node__name--horizontal'), '世系表格缺少英文姓名横排样式')
|
||
expect(!pedigree.includes('\\p{Script=Han}'), '世系表格仍使用 Android WebView 不兼容的正则')
|
||
expect(!treeOverview.includes('邀请绑定暂未开放'), '世系操作仍展示没有实现的占位入口')
|
||
expect(moduleBackground.includes('mode="aspectFill"'), '通用长背景仍按原图高度绘制')
|
||
expect(genealogyBackground.includes('mode="aspectFill"'), '家谱长背景仍按原图高度绘制')
|
||
|
||
expect(vipService.includes('async createVipOrder'), 'VIP service 缺少创建订单接口')
|
||
expect(vipService.includes('async getVipPaymentStatus'), 'VIP service 缺少支付状态查询接口')
|
||
expect(vipService.includes('async closeVipPayment'), 'VIP service 缺少关闭支付接口')
|
||
expect(vipPage.includes('uni.requestPayment'), 'VIP 页面没有接入 App 支付')
|
||
expect(vipPage.includes('createVipOrder'), 'VIP 页面没有调用创建订单接口')
|
||
expect(vipPage.includes('createNonIdempotentWriteGuard'), 'VIP 创建订单缺少重复下单保护')
|
||
expect(vipPage.includes('orderCreationGuard.recordFailure'), 'VIP 下单结果未知时仍允许重复购买')
|
||
expect(Boolean(manifest['app-plus']?.modules?.Payment), 'manifest 未启用 Payment 模块')
|
||
expect(Boolean(manifest['app-plus']?.modules?.Share), 'manifest 未启用推广页所需的 Share 模块')
|
||
expect(
|
||
/saveGenerationPoemBatch[\s\S]*?requireData:\s*false/.test(generationPoemService),
|
||
'字辈批量保存仍把 VoidResult 当作必须包含 data 的响应',
|
||
)
|
||
expect(
|
||
/transferGenealogyOwner[\s\S]*?requireData:\s*false/.test(genealogyMemberService),
|
||
'谱主转让仍把 VoidResult 当作必须包含 data 的响应',
|
||
)
|
||
expect(
|
||
/setGrowthRecordPassword[\s\S]*?requireData:\s*false/.test(lifeRecordService),
|
||
'成长记录密码设置仍把无 data 的成功响应当作失败',
|
||
)
|
||
expect(
|
||
/setPersonDocumentPassword[\s\S]*?requireData:\s*false/.test(personDocumentService),
|
||
'证件密码设置仍把 VoidResult 当作必须包含 data 的响应',
|
||
)
|
||
expect(mediaUpload.includes('IMAGE_TYPE_INVALID'), '图片上传缺少选择结果类型复核')
|
||
expect(mediaUpload.includes('readNativeFile(await pickNativeVideo(), "视频")'), '视频读取错误仍误称为图片错误')
|
||
expect(
|
||
businessFileContract.includes("!/^https:\\/\\/[^\\s]+$/.test(accessUrl)"),
|
||
'业务文件访问地址没有限制为 HTTPS',
|
||
)
|
||
expect(
|
||
authContract.includes("typeof token !== 'string' || !token || token.trim() !== token"),
|
||
'登录响应没有严格校验 access_token',
|
||
)
|
||
expect(!tacLibrary.includes('res.code'), '行为验证码 SDK 异常分支仍引用未定义的 res.code')
|
||
expect(!tacLibrary.includes('validFail(res'), '行为验证码 SDK 异常分支仍传递未定义的 res')
|
||
expect(
|
||
manifest['app-plus']?.distribute?.android?.permissions?.some((permission) =>
|
||
permission.includes('android.permission.READ_MEDIA_VIDEO')),
|
||
'Android 视频相册选择缺少 READ_MEDIA_VIDEO 权限',
|
||
)
|
||
expect(
|
||
manifest['app-plus']?.distribute?.android?.permissions?.some((permission) =>
|
||
permission.includes('android.permission.READ_EXTERNAL_STORAGE') &&
|
||
permission.includes('android:maxSdkVersion="32"')),
|
||
'Android 12L 及以下相册选择缺少受限的存储读取权限',
|
||
)
|
||
expect(
|
||
manifest['app-plus']?.distribute?.ios?.privacyDescription?.NSPhotoLibraryUsageDescription,
|
||
'iOS 相册选择缺少 NSPhotoLibraryUsageDescription 用途说明',
|
||
)
|
||
|
||
expect(
|
||
/promotion-card__actions[^>]*@click\.stop/.test(promotionsPage),
|
||
'推广操作按钮会继续触发卡片跳转',
|
||
)
|
||
expect(
|
||
promotionsPage.includes('<ReferralQrCode :value="referralProfile.shareUrl"') &&
|
||
referralQrCode.includes('class="referral-qr-code"'),
|
||
'推广页没有根据后端 shareUrl 展示推荐二维码',
|
||
)
|
||
expect(
|
||
/\.help-article-list\s*\{[^}]*margin-top:\s*18rpx/.test(helpPage),
|
||
'帮助中心分类栏与首条问答之间缺少间距',
|
||
)
|
||
expect(
|
||
/\.help-feedback-card text\s*\{[^}]*display:\s*block/.test(helpPage),
|
||
'帮助中心反馈标题和说明仍挤在同一行',
|
||
)
|
||
expect(editProfilePage.includes('createDiscardConfirmation'), '编辑资料页缺少未保存修改确认')
|
||
expect(editProfilePage.includes('onBackPress'), '编辑资料页系统返回未接入返回守卫')
|
||
expect(editProfilePage.includes('dirty: isDirty.value'), '编辑资料页返回守卫未检查完整资料快照')
|
||
expect(genealogySettingsPage.includes('createDiscardConfirmation'), '家谱设置页缺少未保存修改确认')
|
||
expect(genealogySettingsPage.includes('onBackPress'), '家谱设置页系统返回未接入返回守卫')
|
||
expect(genealogySettingsPage.includes('if (isDirty.value'), '家谱设置页返回守卫未检查设置快照')
|
||
expect(familyVideosPage.includes('createDiscardConfirmation'), '家族视频表单缺少未保存修改确认')
|
||
expect(familyVideosPage.includes('onBackPress'), '家族视频页系统返回未接入返回守卫')
|
||
expect(familyVideosPage.includes('formSnapshot.value !== formBaseline.value'), '家族视频表单返回未检查视频草稿')
|
||
expect(!familyVideosPage.includes('video-card__player'), '家族视频列表仍直接铺设播放器')
|
||
expect(familyVideosPage.includes('video-card__cover-action'), '家族视频列表没有封面点击播放入口')
|
||
expect(ceremonyListPage.includes('item.coverFile?.accessUrl'), '礼仪列表没有消费封面字段')
|
||
expect(ceremonyDetailPage.includes('detail.coverFile?.accessUrl'), '礼仪详情没有显示封面')
|
||
expect(articleListPage.includes('item.coverFile?.accessUrl'), '谱文列表没有消费封面字段')
|
||
expect(articleDetailPage.includes('article.coverFile?.accessUrl'), '谱文详情没有显示封面')
|
||
expect(routes.includes('path: "/pages/records/person-documents"'), '缺少家谱级重要证件路由')
|
||
expect(personDocumentsPage.includes('<PersonDocumentDialog'), '家谱级重要证件页没有复用证件工作流')
|
||
expect(personDocumentDialog.includes('const documentQuery = props.personId'), '重要证件弹窗不支持家谱级聚合查询')
|
||
expect(vipContract.includes("orderNo: normalizeVipText(item.orderNo, 'orderNo')"), 'VIP 订单契约没有保留订单号')
|
||
expect(vipPage.includes('订单号:{{ item.orderNo }}'), 'VIP 订单页没有显示订单号')
|
||
expect(vipPage.includes('支付时间:{{ item.paidAt }}') && vipPage.includes('到期时间:{{ item.expiresAt }}'), 'VIP 订单页没有区分支付和到期时间')
|
||
for (const field of ['withdrawalNo', 'auditRemark', 'payoutReference', 'paidAt']) {
|
||
expect(earningsPage.includes(`withdrawal.${field}`), `提现记录没有显示 ${field}`)
|
||
}
|
||
expect(relativeRecordsPage.includes('item.mediaFiles?.[0]?.accessUrl'), '贺礼簿列表没有显示首图')
|
||
expect(meritRecordsPage.includes('item.mediaFiles?.[0]?.accessUrl'), '功德记录列表没有显示首图')
|
||
expect(memoPage.includes('item.mediaFiles?.[0]?.accessUrl'), '备忘录和恩人录列表没有显示首图')
|
||
expect(
|
||
platformVideosPage.includes('video-card__cover-placeholder') &&
|
||
!/<video\s+v-else[\s\S]*?class="video-card__player"/.test(platformVideosPage),
|
||
'宣传视频列表无封面时仍直接铺设播放器',
|
||
)
|
||
expect(growthJournalPage.includes('field !== "lineagePersonId"'), '成长记录仍把自动带入人物直接判为用户修改')
|
||
const joinApplicationRoute = routes.match(/G08: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
|
||
expect(joinApplicationRoute.includes('"genealogyName"'), '公开家谱申请页路由仍拒绝谱名参数')
|
||
expect(messageCenterPage.includes(':active="sourceTab"') && messageCenterPage.includes('returnToSource'), '消息中心没有按真实来源恢复导航')
|
||
expect(changePhonePage.includes('请先获取新手机号的验证码'), '换绑手机号仍错误提示当前手机号验证码')
|
||
expect(!changePhonePage.includes('请先获取当前手机号的验证码'), '换绑手机号保留了错误的验证码归属文案')
|
||
expect(changePhonePage.includes('role="alert"') && changePhonePage.includes(':aria-describedby'), '换绑手机号字段错误缺少无障碍关联')
|
||
expect(changePasswordPage.includes('<button') && changePasswordPage.includes(':aria-pressed'), '密码显示开关缺少按钮和状态语义')
|
||
expect(changePasswordPage.includes('role="alert"') && changePasswordPage.includes(':aria-describedby'), '修改密码字段错误缺少无障碍关联')
|
||
expect(
|
||
genealogySettingsPage.indexOf('pageState.value = "form"') < genealogySettingsPage.indexOf('getPermanentDeletionCapability'),
|
||
'家谱设置仍被永久注销资格读取阻断',
|
||
)
|
||
for (const dialogPagePath of dialogPages) {
|
||
expect(read(dialogPagePath).includes('onBackPress'), `${dialogPagePath} 的弹窗未接入系统返回`)
|
||
}
|
||
|
||
const iconPaths = []
|
||
const collectStrings = (value) => {
|
||
if (typeof value === 'string') iconPaths.push(value)
|
||
else if (value && typeof value === 'object') Object.values(value).forEach(collectStrings)
|
||
}
|
||
collectStrings(manifest['app-plus']?.distribute?.icons)
|
||
const uniqueIconPaths = [...new Set(iconPaths)]
|
||
expect(uniqueIconPaths.length === 17, `App 图标路径数量异常:${uniqueIconPaths.length}`)
|
||
for (const iconPath of uniqueIconPaths) {
|
||
expect(fs.existsSync(path.join(workspace, iconPath)), `App 图标不存在:${iconPath}`)
|
||
}
|
||
|
||
const privacyPath = path.join(workspace, 'androidPrivacy.json')
|
||
expect(fs.existsSync(privacyPath), '缺少 androidPrivacy.json')
|
||
if (fs.existsSync(privacyPath)) {
|
||
const privacy = JSON.parse(fs.readFileSync(privacyPath, 'utf8'))
|
||
expect(privacy.prompt === 'template', 'Android 原生隐私提示未使用 template 模式')
|
||
expect(/https:\/\//.test(privacy.message || ''), 'Android 原生隐私提示没有 HTTPS 协议链接')
|
||
expect(typeof privacy.backToExit === 'boolean', 'Android 原生隐私提示 backToExit 必须是布尔值')
|
||
}
|
||
|
||
expect(!read('App.vue').includes("console.log('家谱 App 已启动')"), 'App.vue 仍保留启动调试日志')
|
||
expect(
|
||
/HTTP_ERROR'[\s\S]*?httpStatus\) === 401/.test(requestClient),
|
||
'请求层没有把 HTTP 401 识别为会话失效',
|
||
)
|
||
expect(
|
||
/statusCode < 200[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
|
||
'普通请求收到 HTTP 401 时没有恢复登录状态',
|
||
)
|
||
expect(
|
||
/expectedStatus !== null[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
|
||
'严格请求收到 HTTP 401 时没有恢复登录状态',
|
||
)
|
||
expect(requestClient.includes('const FILE_UPLOAD_TIMEOUT_MS = 120000'), '文件上传没有独立的超时配置')
|
||
expect(
|
||
(requestClient.match(/timeout: FILE_UPLOAD_TIMEOUT_MS|}, FILE_UPLOAD_TIMEOUT_MS\)/g) || []).length === 2,
|
||
'原生和浏览器文件上传没有统一使用独立超时配置',
|
||
)
|
||
expect(
|
||
requestClient.includes('rejectAuthenticatedUploadStatus'),
|
||
'文件上传收到 HTTP 401 时没有恢复登录状态',
|
||
)
|
||
expect(
|
||
/unwrapAuthenticatedUploadResponse[\s\S]*?isAuthenticatedSessionRejected[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
|
||
'文件上传收到业务 401 时没有恢复登录状态',
|
||
)
|
||
expect(
|
||
/HTTP_ERROR'[\s\S]*?httpStatus\) === 403/.test(requestErrorMessage),
|
||
'HTTP 403 没有使用无权限提示',
|
||
)
|
||
for (const [pageName, pageSource] of [
|
||
['成员目录', memberDirectoryPage],
|
||
['人物列表', peoplePage],
|
||
]) {
|
||
expect(pageSource.includes('const requestedPage = append ? pageNum.value + 1 : 1'), `${pageName} 没有延迟提交分页页码`)
|
||
expect(!pageSource.includes('pageNum.value += 1'), `${pageName} 仍会在请求成功前递增页码`)
|
||
expect(pageSource.includes('if (append) loadMoreError.value = true'), `${pageName} 加载更多失败会破坏现有列表`)
|
||
expect(pageSource.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), `${pageName} 没有校验家谱参数`)
|
||
}
|
||
for (const pagePath of idValidatedPages) {
|
||
expect(read(pagePath).includes('/^[1-9]\\d*$/.test(genealogyId.value)'), `${pagePath} 没有校验家谱 ID`)
|
||
}
|
||
expect(memberRankPage.includes('returnTo("T05", { genealogyId, personId })'), '旧成员排行页没有统一转入成员编辑页')
|
||
expect(!memberRankPage.includes('lineageApi.updatePerson'), '旧成员排行页仍保留独立保存逻辑')
|
||
expect(!memberRankPage.includes('<text>排序值</text>'), '独立排行页仍要求普通用户手填排序值')
|
||
expect(!memberRankPage.includes('updatePersonSortOrder'), '独立排行页仍会提交手工排序值')
|
||
expect(editProfilePage.includes('original[field] = payload[field]'), '编辑资料保存成功后没有更新草稿基准')
|
||
expect(meritRecordsPage.includes('/^(?:0|[1-9]\\d{0,9})(?:\\.\\d{1,2})?$/'), '功德记录页面没有限制金额精度和范围')
|
||
expect(lifeRecordContract.includes("normalizeOptionalCurrencyNumber(payload.amount, '功德金额')"), '功德记录 API 契约没有限制金额精度和范围')
|
||
expect(requestNormalizers.includes('normalizeOptionalCurrencyNumber'), '请求契约缺少统一金额校验')
|
||
expect(ceremonyDetailPage.includes('献礼金额应为 0 至 9999999999.99'), '礼仪献礼页面没有限制金额精度和范围')
|
||
expect(relativeRecordEditorPage.includes('礼金金额应为 0 至 9999999999.99'), '亲友往来页面没有限制金额精度和范围')
|
||
expect(profileContract.includes('const isCalendarDate'), '个人资料契约没有验证真实日期')
|
||
expect(profileContract.includes("birthday && !isCalendarDate(birthday.slice(0, 10))"), '个人资料响应没有验证生日')
|
||
expect(genealogyContext.includes("/^[1-9]\\d*$/.test(genealogyId)"), '家谱上下文允许缓存无效家谱 ID')
|
||
expect(
|
||
editMemberPage.match(/\/\^\[1-9\]\\d\*\$\/.test/g)?.length >= 4,
|
||
'修改成员页没有完整校验家谱和人物 ID',
|
||
)
|
||
expect(editMemberPage.includes('lineageApi.getRankOptions'), '修改成员页没有读取排行称谓选项')
|
||
expect(editMemberPage.includes('rankId: editForm.rankId'), '修改成员页没有提交所选排行称谓')
|
||
expect(editMemberPage.includes('暂无可选排行'), '修改成员页没有展示排行配置缺失状态')
|
||
expect(!editMemberPage.includes('<text>排序值</text>'), '修改成员页仍要求普通用户手填排序值')
|
||
expect(addRelativePage.includes('lineageApi.getRankOptions'), '新增亲属页没有读取排行称谓选项')
|
||
expect(addRelativePage.includes('{ rankId: addForm.rankId }'), '新增亲属页没有提交所选排行称谓')
|
||
expect(!addRelativePage.includes('<text>排序值</text>'), '新增亲属页仍要求普通用户手填排序值')
|
||
expect(addRelativePage.includes('MemberDatePickerSheet'), '新增亲属页没有复用成员日期选择器')
|
||
expect(!addRelativePage.includes('mode="date"'), '新增亲属页仍在使用可选择未来年份的系统日期控件')
|
||
expect(addRelativePage.includes('不能晚于今天'), '新增亲属页没有在提交前拦截未来日期')
|
||
expect(!treeOverview.includes('writing-mode: vertical-rl'), '树状图世代信息仍被强制逐字竖排')
|
||
expect(treeOverview.includes('-webkit-line-clamp: 2'), '树状图长姓名仍会被单行省略号截断')
|
||
expect(!editMemberPage.includes('`${name}(${personOptionId})`'), '修改成员页仍向用户暴露人物内部 ID')
|
||
expect(editMemberPage.includes('sortOrder: editForm.sortOrder'), '隐藏排序值后修改成员会把原顺序重置为零')
|
||
expect(!treeOverview.includes('@click="toRank"') && !treeOverview.includes('ADJUST_RANK'), '树状图仍保留重复的独立排行入口')
|
||
expect(
|
||
/v-if="feed\.canDelete"[\s\S]*?label="删除动态"/.test(feedDetailPage),
|
||
'动态详情向无删除权限用户显示删除按钮',
|
||
)
|
||
expect(
|
||
articleDetailPage.includes('article.canEdit || article.canDelete || article.canManageProtection'),
|
||
'谱文仅有内容密码管理权限时不显示密码操作',
|
||
)
|
||
expect(
|
||
/v-if="video\.canDelete"[\s\S]*?:label="deletingVideoId/.test(familyVideosPageSource),
|
||
'视频列表向无删除权限用户显示删除按钮',
|
||
)
|
||
expect(genealogyContract.includes('hasMembership: item.canManage === true || Boolean(memberStatus)'), '公开家谱契约用管理权限代替成员关系')
|
||
expect(genealogySearchPage.includes(':disabled="item.hasMembership"'), '公开家谱对普通成员仍显示申请加入')
|
||
expect(treeOverview.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), '树状图没有校验家谱 ID')
|
||
expect(pedigree.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), '世系谱没有校验家谱 ID')
|
||
|
||
const articleCategoryContract = openApi.slice(
|
||
openApi.indexOf(' /genealogy/app/genealogies/{genealogyId}/article-categories:'),
|
||
openApi.indexOf(' /genealogy/app/genealogies/{genealogyId}/articles:'),
|
||
)
|
||
expect(articleCategoryContract.includes('AppArticleCategoryResult'), '谱文分类写接口没有返回明确的分类对象')
|
||
expect(!articleCategoryContract.includes('PaymentOrderVo'), '谱文分类接口仍错误引用支付下单模型')
|
||
|
||
expect(/loginWithWechat[\s\S]*?data:\s*\{\s*code:/.test(authService), '微信登录没有按最新后端契约只提交一次性 code')
|
||
expect(authService.includes('ACCOUNT_BINDING_REQUIRED'), '微信登录没有处理后端要求的账号绑定状态')
|
||
expect(securityPage.includes('authApi.bindWechat'), '账号与安全页没有提供微信绑定入口')
|
||
expect(contentRecoveryService.includes('/capability`'), '内容密码找回能力仍调用旧路径')
|
||
expect(/resetPassword[\s\S]*?method:\s*'POST'/.test(contentRecoveryService), '内容密码重置仍使用旧 HTTP 方法')
|
||
expect(/sendCode[\s\S]*?requireData:\s*false/.test(contentRecoveryService), '内容密码验证码发送仍要求后端返回旧版 delivery 对象')
|
||
expect(contentRecoveryContract.includes('value.available'), '内容密码找回能力仍读取旧版 enabled 字段')
|
||
expect(contentRecoveryContract.includes('value.mobileMasked'), '内容密码找回能力仍读取旧版 maskedPhone 字段')
|
||
expect(referralContract.includes('referredUserCount'), '推广资料没有接入后端返回的推荐人数')
|
||
expect(referralContract.includes('value.shareUrl'), '推广资料没有读取后端提供的 HTTPS 分享链接')
|
||
expect(promotionsPage.includes('profile.shareUrl'), '推广中心分享内容没有使用后端提供的分享链接')
|
||
expect(profileContract.includes('currentPassword: payload.currentPasswordHash.toLowerCase()'), '手机号换绑没有提交当前密码摘要')
|
||
expect(changePhonePage.includes('calcMD5(currentPassword.value)'), '手机号换绑没有按现有认证契约处理当前密码')
|
||
expect(changePhonePage.includes('session.clear()') && changePhonePage.includes('goRoot("A01")'), '手机号换绑成功后没有清理登录态并返回登录页')
|
||
expect(changePasswordPage.includes('session.clear()') && changePasswordPage.includes('goRoot("A01")'), '密码修改成功后没有清理已失效登录态')
|
||
expect(genealogyContract.includes("'requestId'") && genealogyContract.includes("'ownerIsFirstAncestor'"), '建谱契约缺少幂等请求号或谱主始迁祖标记')
|
||
expect(genealogyCreatePage.includes('genealogyCreateRequestId') && genealogyCreatePage.includes('ownerIsFirstAncestor'), '建谱页面没有提交稳定请求号或谱主始迁祖选择')
|
||
expect(genealogyContract.includes('normalizedPayload.coverOssId = null'), '家谱封面契约不能发送 null 清空')
|
||
for (const [pageSource, pageName] of [[genealogySettingsPage, '家谱'], [articleEditorPage, '谱文'], [ceremonyEditorPage, '礼仪'], [familyVideosPage, '视频']]) {
|
||
expect(pageSource.includes('移除封面'), `${pageName}编辑页没有提供移除封面操作`)
|
||
}
|
||
expect(lifeRecordContract.includes("BENEFACTOR: 'benefactor'"), '备忘契约缺少家族恩人类型')
|
||
expect(memoPage.includes('memoType: memoType.value'), '家族恩人页面没有提交 benefactor 类型')
|
||
expect(memoPage.includes('memo.memoType === memoType.value'), '家族恩人列表没有按类型过滤')
|
||
for (const contractSource of [lifeRecordContract, familyMediaContract, read('services/api/ceremony-contract.js')]) {
|
||
expect(contractSource.includes('createTime:'), '资源响应契约没有保留后端 createTime')
|
||
}
|
||
expect(batchDeletionComposable.includes('for (const id of pendingIds)') && batchDeletionComposable.includes('break'), '批量删除没有按顺序执行或遇到失败后停止')
|
||
expect(batchDeletionComposable.includes('selectedIds.value = selectedIds.value.filter'), '批量删除成功后没有移除已完成选择')
|
||
expect(batchManagementBar.includes('仅可选择有删除权限的内容'), '批量管理没有说明权限边界')
|
||
expect(batchSelectionMark.includes('role="checkbox"') && batchSelectionMark.includes(':aria-checked="selected"'), '批量选择控件缺少复选语义')
|
||
for (const [pageSource, resourceName] of [
|
||
[articleListPage, '谱文'],
|
||
[familyVideosPage, '视频'],
|
||
[ceremonyListPage, '礼仪活动'],
|
||
[meritRecordsPage, '功德记录'],
|
||
[relativeRecordsPage, '往来记录'],
|
||
[memoPage, '家族备忘/恩人'],
|
||
[growthJournalPage, '成长记录'],
|
||
[personDocumentDialog, '重要证件'],
|
||
]) {
|
||
expect(pageSource.includes('useBatchDeletion'), `${resourceName}没有接入统一批量删除状态`)
|
||
expect(pageSource.includes('BatchManagementBar') && pageSource.includes('BatchSelectionMark'), `${resourceName}缺少批量管理入口或选择控件`)
|
||
}
|
||
expect(/createArticle[\s\S]*?normalizeAppArticle\(article, normalizedGenealogyId\)/.test(familyArticleService), '谱文创建响应没有规范化,无法可靠继续设置密码')
|
||
expect(/createGrowthRecord[\s\S]*?normalizeAppGrowthRecord\(growthRecord, normalizedGenealogyId\)/.test(lifeRecordService), '成长记录创建响应没有规范化,无法可靠继续设置密码')
|
||
expect(/genealogyNo:\s*normalizeGenealogyResponseText\(item\.genealogyNo/.test(genealogyContract), '我的家谱响应没有保留后端返回的稳定家谱编号')
|
||
expect(genealogyContract.includes("createTime: normalizeGenealogyResponseText(item.createTime, 'createTime')"), '我的家谱响应没有保留后端真实创建时间')
|
||
expect(genealogyContract.includes('createTime: genealogy.createTime'), '我的家谱列表规范化时丢弃了真实创建时间')
|
||
expect(familyArticleContract.includes("'mediaOssIds'") && familyArticleContract.includes('normalizedPayload.mediaOssIds'), '谱文请求契约没有提交正文图片标识')
|
||
expect(familyArticleContract.includes('mediaFiles: normalizeBusinessFileAccessRows'), '谱文响应契约没有保留正文图片')
|
||
expect(articleEditorPage.includes('mediaReceipts') && articleEditorPage.includes('mediaOssIds') && articleEditorPage.includes('removeArticleMedia'), '谱文编辑页没有完整支持正文多图新增、回显和移除')
|
||
expect(articleDetailPage.includes(':files="article.mediaFiles"'), '谱文详情没有按后端顺序显示正文图片')
|
||
expect(/createArticle[\s\S]*?setArticlePassword/.test(articleEditorPage), '新建谱文没有在创建成功后继续设置内容密码')
|
||
expect(/createGrowthRecord[\s\S]*?setGrowthRecordPassword/.test(growthJournalPage), '新建成长记录没有在创建成功后继续设置内容密码')
|
||
expect(/createPersonDocument[\s\S]*?setPersonDocumentPassword/.test(personDocumentDialog), '新建重要证件没有在创建成功后继续设置内容密码')
|
||
for (const protectedCreateSource of [articleEditorPage, growthJournalPage, personDocumentDialog]) {
|
||
expect(protectedCreateSource.includes('可能尚未受到密码保护'), '两阶段创建没有明确提示密码设置失败后的未保护风险')
|
||
}
|
||
expect(permissionContract.includes('item.code'), '权限目录仍读取旧版 permissionCode 字段')
|
||
expect(permissionContract.includes('item.groupName'), '权限目录没有接入后端权限分组')
|
||
expect(genealogyCapabilityService.includes('/comments/page`'), '家族视频根评论仍调用旧列表路径')
|
||
expect(genealogyCapabilityService.includes('/replies/page`'), '家族视频回复仍调用旧列表路径')
|
||
expect(/getVideoComments[\s\S]*?assertPage/.test(genealogyCapabilityService), '家族视频评论没有读取后端分页响应')
|
||
expect(genealogyContract.includes('genealogyName: payload.confirmationName.trim()'), '家谱永久删除仍提交旧版 confirmationName 字段')
|
||
expect(/getPermanentDeletionCapability[\s\S]*?permanent-deletion\/capability/.test(vipService + genealogyCapabilityService + read('services/api/genealogy-service.js')), '家谱设置没有接入永久删除能力接口')
|
||
expect(vipContract.includes('item.method'), 'VIP 支付能力没有读取后端 VO 的 method 字段')
|
||
expect(!vipContract.includes('item.paymentMethod'), 'VIP 支付能力仍读取与后端 VO 不一致的 paymentMethod 字段')
|
||
expect(vipContract.includes('value.orderString'), '支付宝下单仍读取旧版 alipayOrderInfo 字段')
|
||
expect(vipContract.includes('value.completed !== true'), '余额支付没有校验后端 completed 字段')
|
||
expect(/createVipOrder[\s\S]*?requestId/.test(vipService), 'VIP 下单没有提交后端必填幂等 requestId')
|
||
for (const dictionaryType of [
|
||
'gen_parent_relationship_variant',
|
||
'gen_education_type',
|
||
'gen_death_expression',
|
||
'gen_spouse_relationship_variant',
|
||
'gen_ceremony_type',
|
||
'gen_person_document_type',
|
||
'gen_growth_record_type',
|
||
'gen_merit_type',
|
||
'gen_feedback_type',
|
||
'gen_zodiac',
|
||
]) {
|
||
expect(businessDictionaryContract.includes(`'${dictionaryType}'`), `前端业务字典白名单缺少 ${dictionaryType}`)
|
||
}
|
||
for (const latestLineageField of ['zodiacCode', 'educationCode', 'deathExpressionCode', 'relationVariantCode']) {
|
||
expect(lineageWriteContract.includes(`'${latestLineageField}'`), `成员写契约缺少最新字段 ${latestLineageField}`)
|
||
}
|
||
expect(!lineageWriteContract.includes("'hereditaryMedicalHistory'"), '成员主档仍混入遗传病史敏感字段')
|
||
expect(lineageService.includes('/sensitive-profile`'), '成员敏感健康资料没有接入独立接口')
|
||
expect(lineagePersonContract.includes('value.zodiacCode'), '成员详情仍读取旧版生肖字段')
|
||
expect(lineagePersonContract.includes('value.educationCode'), '成员详情仍读取旧版学历字段')
|
||
expect(lineagePersonContract.includes('value.deathExpressionCode'), '成员详情仍读取旧版逝世表述字段')
|
||
for (const pageSource of [addRelativePage, editMemberPage]) {
|
||
expect(pageSource.includes('gen_zodiac'), '成员表单没有读取生肖动态字典')
|
||
expect(pageSource.includes('gen_education_type'), '成员表单没有读取学历动态字典')
|
||
expect(pageSource.includes('gen_death_expression'), '成员表单没有读取逝世表述动态字典')
|
||
expect(pageSource.includes('dictionaryRequestControllers'), '成员表单的并发字典请求仍共用一个取消控制器')
|
||
}
|
||
expect(
|
||
genealogySettingsPage.includes('deletionCapabilityReadRequestController'),
|
||
'家谱设置和永久删除能力的并发读取仍共用一个取消控制器',
|
||
)
|
||
|
||
if (failures.length) {
|
||
console.error(`AUDIT REGRESSION CHECK FAILED (${failures.length})`)
|
||
failures.forEach((failure) => console.error(`- ${failure}`))
|
||
process.exit(1)
|
||
}
|
||
|
||
console.log(`AUDIT REGRESSION CHECK PASS icons=${uniqueIconPaths.length}`)
|