Files
jiapuapp/tests/nm-page-business-contract.ps1
T
2026-07-23 08:24:07 +08:00

166 lines
8.8 KiB
PowerShell

$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$contracts = [ordered]@{
'pages/notification/n01-message-center.vue' = @('listNotificationFixtures', 'notices', 'markAllRead', 'openNotice', 'openPage(', '"N02",', '"G10",')
'pages/notification/n02-message-detail.vue' = @('findNotificationFixture', 'noticeDetail', 'markAsRead', 'openNoticeTarget', 'targetType', 'targetParams', 'notice-state--expired', 'notice-state--ready')
'pages/profile/m01-profile-home.vue' = @('menuItems', 'routeKey', 'openPage(', '"M02"', '"N01"')
'pages/profile/m02-edit-profile.vue' = @('profileForm', 'chooseAvatar', 'validateProfile', 'saveProfile', 'profile-state--saving')
'pages/profile/m03-security-settings.vue' = @('securityItems', 'openSecurityItem', 'device-state--limited', 'checkSecurity', 'routeKey: "M04"', 'routeKey: "M05"')
'pages/profile/m04-change-password.vue' = @('passwordForm', 'validatePassword', 'togglePassword', 'savePassword', 'password-state--saving')
'pages/profile/m05-change-phone.vue' = @('phoneForm', 'sendCode', 'savePhone', 'phone-state--saving')
'pages/profile/m06-help-center.vue' = @('helpCategories', 'filteredQuestions', 'toggleQuestion', 'contactSupport', 'openPage("M07", {}, "M06")')
'pages/profile/m07-feedback.vue' = @('feedbackForm', 'feedbackTypes', 'validateFeedback', 'submitFeedback', 'feedback-state--submitting')
'pages/profile/m08-promotion.vue' = @('inviteState', 'openInviteExplanation', 'share-state--unavailable', 'explanationVisible')
'pages/profile/m09-vip-orders.vue' = @('serviceBenefits', 'orderState', 'order-state--unavailable', 'openServiceNotice')
'pages/profile/m10-about-settings.vue' = @('agreementItems', 'openAgreement', 'confirmLogout', 'appVersion', 'logoutVisible')
}
foreach ($entry in $contracts.GetEnumerator()) {
$source = Get-Content -LiteralPath (Join-Path $root $entry.Key) -Raw -Encoding UTF8
if ($source -match '<ModulePage(?:\s|/|>)|import\s+ModulePage\s+from') { throw "$($entry.Key) must not consume ModulePage" }
foreach ($forbidden in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet', 'uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch', 'getCurrentPages(', '/pages/')) {
if ($source.Contains($forbidden)) { throw "$($entry.Key) contains forbidden token: $forbidden" }
}
if ($entry.Key -ne 'pages/profile/m07-feedback.vue' -and $source.Contains('@/utils/api.js')) {
throw "$($entry.Key) must not consume the API before its independent interface batch"
}
foreach ($required in @('ModulePageBackground', 'PageHeader', 'AppButton') + $entry.Value) {
if (-not $source.Contains($required)) { throw "$($entry.Key) missing business token: $required" }
}
if ($source -notmatch 'min-height:\s*100vh') { throw "$($entry.Key) must use natural page height" }
}
$sources = @{}
foreach ($path in $contracts.Keys) {
$sources[$path] = Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding UTF8
}
$n01 = $sources['pages/notification/n01-message-center.vue']
$n02 = $sources['pages/notification/n02-message-detail.vue']
$m02 = $sources['pages/profile/m02-edit-profile.vue']
$m03 = $sources['pages/profile/m03-security-settings.vue']
$m08 = $sources['pages/profile/m08-promotion.vue']
$m10 = $sources['pages/profile/m10-about-settings.vue']
if ($n01 -notmatch '(?s)openPage\(\s*"N02",\s*\{ id: String\(item\.id\) \},\s*"N01",?\s*\)') {
throw 'N01 must open N02 with only its registered lexical notice identity'
}
if ($n01 -notmatch '(?s)genealogyId\.value\s*\?\s*openPage\(\s*"G10",\s*\{ genealogyId: genealogyId\.value \},\s*"N01",?\s*\)') {
throw 'N01 review entry must fail closed without genealogyId and use the gateway'
}
foreach ($source in @($n01, $n02)) {
foreach ($forbiddenTarget in @('detailId', 'target: "/pages/', 'targetUrl', 'returnUrl', 'const noticeDetails')) {
if ($source.Contains($forbiddenTarget)) { throw "Notification pages retain a duplicate identity, owner, or raw target: $forbiddenTarget" }
}
}
if ($n02 -match 'noticeDetail\.value\.target(?![A-Za-z0-9_$])') {
throw 'N02 retains an executable target field'
}
if ($n02.Contains('noticeDetail.read') -or -not $n02.Contains('noticeDetail.unread')) {
throw 'N02 must consume the notification owner unread field instead of creating a duplicate read field'
}
if ($n02 -notmatch '(?s)const selectedNotice = findNotificationFixture\(noticeId\.value\);.*?if \(.*?!selectedNotice.*?\) \{.*?noticeState\.value = "expired";.*?return;') {
throw 'N02 unknown notice identity must fail closed instead of reusing a default notice'
}
foreach ($required in @(
'getGenealogyFixtureAccess',
'targetError',
'navigateNoticeTarget(',
'noticeDetail.value.targetType',
'noticeDetail.value.targetParams'
)) {
if (-not $n02.Contains($required)) { throw "N02 safe target flow missing: $required" }
}
foreach ($formPath in @(
'pages/profile/m02-edit-profile.vue',
'pages/profile/m04-change-password.vue',
'pages/profile/m05-change-phone.vue',
'pages/profile/m07-feedback.vue'
)) {
$source = $sources[$formPath]
foreach ($required in @(
'custom-back',
'@back="requestBack"',
'createDiscardConfirmation',
'const isDirty = computed(',
'runBackGuard({',
'dirty: isDirty.value',
'submitting:',
'onBackPress((event) => handleBackPress(event, requestBack));',
'discardConfirmation.dispose();',
':close-on-mask="false"'
)) {
if (-not $source.Contains($required)) { throw "$formPath back-guard contract missing: $required" }
}
}
foreach ($previewContract in @(
@{ Path = 'pages/profile/m02-edit-profile.vue'; Retired = @('个人资料已保存') },
@{ Path = 'pages/profile/m04-change-password.vue'; Retired = @('密码已修改', 'passwordForm.current = ""', 'passwordForm.next = ""', 'passwordForm.confirm = ""') },
@{ Path = 'pages/profile/m05-change-phone.vue'; Retired = @('演示验证码已发送', '绑定手机号已更新', 'phoneForm.currentPhone =') }
)) {
$source = $sources[$previewContract.Path]
if (-not $source.Contains('尚未提交服务器')) {
throw "$($previewContract.Path) must identify local validation as unsaved"
}
foreach ($retired in $previewContract.Retired) {
if ($source.Contains($retired)) { throw "$($previewContract.Path) retains fake server success: $retired" }
}
}
foreach ($dialogPath in @(
'pages/profile/m08-promotion.vue',
'pages/profile/m09-vip-orders.vue',
'pages/profile/m10-about-settings.vue'
)) {
$source = $sources[$dialogPath]
foreach ($required in @(
'custom-back',
'@back="requestBack"',
'runBackGuard({',
'transientOpen:',
'onBackPress((event) => handleBackPress(event, requestBack));',
':close-on-mask="false"'
)) {
if (-not $source.Contains($required)) { throw "$dialogPath transient back-guard contract missing: $required" }
}
}
foreach ($retiredInviteCopy in @('仍需管理员审核才能加入', '家人提交加入申请', '管理员核实并审核', 'inviteCode', 'setClipboardData', 'posterVisible', 'generatePoster')) {
if ($m08.Contains($retiredInviteCopy)) { throw "M08 retains retired invitation review semantics: $retiredInviteCopy" }
}
foreach ($directInviteCopy in @('邀请码校验成功后直接加入', '不会生成入谱审核记录')) {
if (-not $m08.Contains($directInviteCopy)) { throw "M08 direct-join contract missing: $directInviteCopy" }
}
foreach ($unavailableInviteCopy in @('当前没有可用邀请码', '尚未接入邀请码校验')) {
if (-not $m08.Contains($unavailableInviteCopy)) { throw "M08 unavailable boundary missing: $unavailableInviteCopy" }
}
if (-not $m03.Contains('currentUser.phone') -or $m03.Contains('139****6421')) {
throw 'M03 must consume the unique current-user phone instead of a second hard-coded identity'
}
if (-not $m02.Contains('currentUser.name') -or $m02.Contains('nickName: "汤文远"')) {
throw 'M02 must initialize editable identity from the unique current-user owner'
}
$m09 = $sources['pages/profile/m09-vip-orders.vue']
foreach ($forbiddenOrderPreview in @('query.state', '演示订单', '¥0.00', 'order-state--ready')) {
if ($m09.Contains($forbiddenOrderPreview)) { throw "M09 must not fabricate an order before API integration: $forbiddenOrderPreview" }
}
if ($m10 -notmatch 'import \{ session \} from "@/utils/session\.js";') {
throw 'M10 must consume the unique session owner'
}
if ($m10 -notmatch '(?s)const confirmLogout = \(\) => \{\s*session\.clear\(\);\s*logoutVisible\.value = false;\s*return goRoot\("A01"\);\s*\};') {
throw 'M10 logout order must be session.clear -> close dialog -> goRoot(A01)'
}
if (([regex]::Matches($m10, 'session\.clear\(\)')).Count -ne 1) {
throw 'M10 cancellation or back paths must never clear the session'
}
if ($m10 -notmatch 'import manifest from "@/manifest\.json";' -or $m10.Contains('const appVersion = "1.0.0"')) {
throw 'M10 must display the version from manifest.json instead of duplicating release metadata'
}
Write-Output 'NM-PAGE-BUSINESS-CONTRACT PASS'