Files
jiapuapp/tests/a01-a02-ui-contract.ps1
T
2026-07-27 06:50:23 +08:00

257 lines
14 KiB
PowerShell

$ErrorActionPreference = 'Stop'
function ConvertFrom-Utf8Base64 {
param([string]$Value)
return [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Value))
}
function Assert-Contains {
param(
[string]$Content,
[string]$Expected,
[string]$Message
)
if ($Content -notmatch [regex]::Escape($Expected)) {
throw $Message
}
}
function Assert-NotContains {
param(
[string]$Content,
[string]$Unexpected,
[string]$Message
)
if ($Content -match [regex]::Escape($Unexpected)) {
throw $Message
}
}
$root = Split-Path -Parent $PSScriptRoot
$entryPath = Join-Path $root 'pages/auth/a01-entry.vue'
$shellPath = Join-Path $root 'components/AuthPageShell.vue'
$legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json'
$globalStylesPath = Join-Path $root 'styles/global.scss'
$profilePath = Join-Path $root 'styles/adaptive-frame-profiles.scss'
if (-not (Test-Path -LiteralPath $entryPath)) {
throw 'Missing the single A01 login page.'
}
if (Test-Path -LiteralPath $legacyLoginPath) {
throw 'A02 was merged into A01, so the legacy page file must be removed.'
}
$pages = Get-Content -LiteralPath $pagesPath -Raw -Encoding utf8 | ConvertFrom-Json
if ($pages.pages[0].path -ne 'pages/auth/a01-entry') {
throw 'A01 must remain the first application route.'
}
if ($pages.pages.path -contains 'pages/auth/a02-login') {
throw 'pages.json must not retain the legacy A02 route.'
}
if ($pages.pages.Count -ne 52) {
throw "Expected 52 active routes after the A02 merge and A06 archive, found $($pages.pages.Count)."
}
# A01 是 A02 退役边界的唯一所有者;所有仍保留的认证源码都不得重新引用旧登录页。
foreach ($relativePath in @(
'pages/auth/a01-entry.vue',
'pages/auth/a04-register.vue',
'pages/auth/a05-reset-password.vue',
'pages/auth/a06-auth-status.vue'
)) {
$authSource = Get-Content -LiteralPath (Join-Path $root $relativePath) -Raw -Encoding utf8
Assert-NotContains -Content $authSource -Unexpected '/pages/auth/a02-login' -Message "Legacy A02 navigation remains in $relativePath."
}
$entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8
$shell = Get-Content -LiteralPath $shellPath -Raw -Encoding utf8
$globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8
$profiles = Get-Content -LiteralPath $profilePath -Raw -Encoding utf8
# A01 owns content in normal document flow. Decorative artwork must not become
# a second full-page coordinate system.
foreach ($required in @(
'class="auth-shell__header"',
'class="auth-shell__header-image"',
'a01-vnext-header-v1.png',
'mode="widthFix"',
'class="auth-shell__paper"',
'auth-page-paper.jpg'
)) {
Assert-Contains -Content $shell -Expected $required -Message "A01 adaptive shell is missing: $required"
}
Assert-Contains -Content $entry -Expected '<AuthPageShell' -Message 'A01 must consume the shared adaptive shell.'
foreach ($forbidden in @(
'class="page-canvas"',
'class="page-backdrop"',
'mode="scaleToFill"',
'--auth-paper-start',
'1665rpx'
)) {
Assert-NotContains -Content $entry -Unexpected $forbidden -Message "A01 still uses rejected page coordinates: $forbidden"
Assert-NotContains -Content $shell -Unexpected $forbidden -Message "The adaptive shell uses rejected page coordinates: $forbidden"
}
Assert-Contains -Content $shell -Expected 'a01-red-hall-ink-backdrop-v1.png' -Message 'The shared auth shell must preserve the approved paper scenery as decoration.'
$pageRule = [regex]::Match($shell, '(?ms)^\.auth-shell\s*\{(?<Body>.*?)^\}')
$headerRule = [regex]::Match($shell, '(?ms)^\.auth-shell__header\s*\{(?<Body>.*?)^\}')
$paperRule = [regex]::Match($shell, '(?ms)^\.auth-shell__paper\s*\{(?<Body>.*?)^\}')
$contentRule = [regex]::Match($entry, '(?ms)^\.login-content\s*\{(?<Body>.*?)^\}')
foreach ($rule in @($pageRule, $headerRule, $paperRule, $contentRule)) {
if (-not $rule.Success) { throw 'A01 adaptive layout rule is missing.' }
}
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'display: flex;' -Message 'A01 root must use flex flow.'
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 root must stack header and paper.'
Assert-Contains -Content $pageRule.Groups['Body'].Value -Expected 'min-height: var(--app-viewport-height);' -Message 'A01 root must fill without locking content height.'
if ($pageRule.Groups['Body'].Value -match '(?m)^\s*height:\s*var\(--app-viewport-height\);') {
throw 'A01 root must not lock content to the viewport.'
}
Assert-Contains -Content $headerRule.Groups['Body'].Value -Expected 'height: calc(var(--app-safe-top) + min(36.5vw, 175px));' -Message 'A01 header must use the approved compact visible slot without distorting its image.'
Assert-Contains -Content $headerRule.Groups['Body'].Value -Expected 'overflow: hidden;' -Message 'A01 header must crop only the excess lower doorway inside its decorative slot.'
Assert-Contains -Content $paperRule.Groups['Body'].Value -Expected 'flex: 1;' -Message 'A01 paper must receive remaining viewport space.'
Assert-Contains -Content $contentRule.Groups['Body'].Value -Expected 'justify-content: space-between;' -Message 'A01 must distribute spare height in normal flow.'
Assert-NotContains -Content $contentRule.Groups['Body'].Value -Unexpected 'grid-area:' -Message 'A01 interactive content must not overlap page artwork.'
# Typography has one owner, and the title decoration participates in normal flow.
Assert-Contains -Content $globalStyles -Expected 'font-family: "STKaiti", "KaiTi", serif' -Message 'Global styles must own the STKaiti/KaiTi font contract.'
Assert-NotContains -Content $entry -Unexpected 'font-family:' -Message 'A01 must inherit the global font contract instead of overriding it locally.'
$headingRule = [regex]::Match($entry, '(?ms)^\.login-heading\s*\{(?<Body>.*?)^\}')
if (-not $headingRule.Success) {
throw 'A01 is missing the login heading style rule.'
}
Assert-Contains -Content $headingRule.Groups['Body'].Value -Expected 'flex-direction: column;' -Message 'A01 login heading must lay out its title and divider vertically in normal flow.'
Assert-NotContains -Content $headingRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login heading must not use positioning for ordinary vertical layout.'
$titleRule = [regex]::Match($entry, '(?ms)^\.login-title\s*\{(?<Body>.*?)^\}')
if (-not $titleRule.Success) {
throw 'A01 is missing the login title style rule.'
}
Assert-NotContains -Content $titleRule.Groups['Body'].Value -Unexpected 'position:' -Message 'A01 login title must remain in normal flow.'
$dividerRule = [regex]::Match($entry, '(?ms)^\.title-divider\s*\{(?<Body>.*?)^\}')
if (-not $dividerRule.Success) {
throw 'A01 is missing the title divider style rule.'
}
foreach ($positioning in @('position: absolute', 'left: 50%', 'translateX(')) {
Assert-NotContains -Content $dividerRule.Groups['Body'].Value -Unexpected $positioning -Message "A01 title divider must not use manual positioning: $positioning"
}
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'width: 280rpx;' -Message 'A01 compact title divider must remain visible on paper.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 46rpx;' -Message 'A01 compact title divider must retain a legible knot and line weight.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'filter: brightness(0.68) saturate(1.5) contrast(1.2);' -Message 'A01 title divider must keep sufficient contrast against the paper background.'
$brandSealRule = [regex]::Match($shell, '(?ms)^\.auth-shell__seal\s*\{(?<Body>.*?)^\}')
if (-not $brandSealRule.Success) {
throw 'A01 is missing the brand seal style rule.'
}
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'position: absolute;' -Message 'A01 brand seal must be scoped as decoration inside the header.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'left: 50%;' -Message 'A01 brand seal must remain centered inside the header.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'transform: translateX(-50%);' -Message 'A01 brand seal must use header-local centering.'
# Final decorative surfaces must come from real bitmap assets.
foreach ($asset in @(
'a01-vnext-header-v1.png',
'auth-page-paper.jpg',
'brand-seal.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-toast-v3.png',
'a01-scroll-dialog-v3.png',
'a01-icon-phone-v1.png',
'a01-icon-lock-v1.png',
'a01-icon-eye-open-v1.png',
'a01-icon-eye-closed-pupil-v2.png',
'a01-icon-sms-code-v2.png',
'a02-agreement-unchecked.png',
'a02-agreement-checked.png'
)) {
Assert-Contains -Content ($entry + $shell + $profiles) -Expected $asset -Message "A01 is missing asset reference: $asset"
}
foreach ($obsoleteVisualAsset in @(
'auth-header.png',
'a01-paper-transition-v1.png',
'a01-login-header-v1.png',
'a01-login-header-exact-v2.png',
'a01-login-scroll-frame-v1.png',
'a01-vnext-scroll-v1.png',
'auth-ink-scenery.png',
'a01-title-divider-v2.png',
'a01-primary-button-v2.png',
'a01-secondary-button-v2.png',
'a02-login-panel.png'
)) {
Assert-NotContains -Content $entry -Unexpected $obsoleteVisualAsset -Message "A01 must not retain the rejected header, scenery, or scroll asset: $obsoleteVisualAsset"
}
$buttonSkinTags = [regex]::Matches($entry, '<image\s+class="button-skin"[^>]+>')
if ($buttonSkinTags.Count -ne 1) {
throw "A01 must have exactly one supported login button skin, found $($buttonSkinTags.Count)."
}
foreach ($buttonSkinTag in $buttonSkinTags) {
Assert-Contains -Content $buttonSkinTag.Value -Expected 'mode="aspectFit"' -Message 'A01 button skins must use uniform aspectFit rendering.'
Assert-NotContains -Content $buttonSkinTag.Value -Unexpected '/opaque/' -Message 'A01 visible button skins must use the current transparent v3 assets.'
Assert-NotContains -Content $buttonSkinTag.Value -Unexpected 'mode="scaleToFill"' -Message 'A01 button skins must not distort fixed artwork with scaleToFill.'
}
$requiredCopy = @(
'55m75b2V5a626LCx',
'5a+G56CB55m75b2V',
'6aqM6K+B56CB55m75b2V',
'5omL5py65Y+3',
'5a+G56CB',
'6aqM6K+B56CB',
'5b+Y6K6w5a+G56CB',
'6I635Y+W6aqM6K+B56CB',
'6L+Y5rKh5pyJ6LSm5Y+377yf',
'5rOo5YaM6LSm5Y+3',
'44CK55So5oi35Y2P6K6u44CL',
'44CK6ZqQ56eB5pS/562W44CL'
) | ForEach-Object { ConvertFrom-Utf8Base64 $_ }
foreach ($copy in $requiredCopy) {
Assert-Contains -Content $entry -Expected $copy -Message "A01 is missing required visible copy: $copy"
}
$smsCodeCopy = ConvertFrom-Utf8Base64 '55+t5L+h6aqM6K+B56CB'
Assert-Contains -Content $entry -Expected $smsCodeCopy -Message 'A01 SMS state must use the full SMS code field label.'
foreach ($contract in @(
'const activeLoginMethod = ref("password")',
'const passwordVisible = ref(false)',
'const switchLoginMethod = (method) =>',
'const togglePasswordVisibility = () =>',
'<AppToast :visible="feedbackVisible" :message="feedbackMessage" />',
'<TacVerification',
'AUTH_VERIFICATION_OPERATION.SMS_LOGIN',
'normalizeTacSuccess',
'appApi.loginWithPassword',
'appApi.loginWithSms',
'calcMD5(password.value)',
'const preparePasswordLogin = async () =>',
'login-submit',
'class="agreement-error"',
'const agreementError = ref(false)'
)) {
Assert-Contains -Content $entry -Expected $contract -Message "A01 is missing state or interaction contract: $contract"
}
Assert-NotContains -Content $entry -Unexpected 'login-tab--unavailable' -Message 'A01 密码登录页签不得继续显示为不可用。'
Assert-NotContains -Content $entry -Unexpected 'PASSWORD_TAC_BLOCKED_MESSAGE' -Message 'A01 密码登录不得继续被旧硬关闭文案阻断。'
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
Assert-NotContains -Content $entry -Unexpected $nativeUi -Message "A01 must not use native UniApp feedback: $nativeUi"
}
Assert-NotContains -Content $entry -Unexpected "url: '/pages/auth/a02-login" -Message 'A01 must not navigate to A02.'
Assert-NotContains -Content $entry -Unexpected 'brand-intro' -Message 'A01 must remove the legacy oversized brand introduction.'
Assert-NotContains -Content $entry -Unexpected 'min-height: 100vh' -Message 'A01 must not depend on 100vh for its page shell.'
Assert-NotContains -Content $entry -Unexpected 'exact-chrome-overlay' -Message 'A01 must not stack a second full-screen scroll perimeter over the complete scroll frame.'
Assert-NotContains -Content $entry -Unexpected 'exact-header-overlay' -Message 'A01 must not stack a second header bitmap over the selected backdrop.'
Assert-NotContains -Content $entry -Unexpected 'auth-design-scenery-v1.png' -Message 'A01 must not use the scenery bitmap that contains stray scroll handles.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-eye-closed-v1.png' -Message 'A01 hidden-password icon must retain the center pupil instead of using the hollow crossed eye.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-verification-v1.png' -Message 'A01 SMS field must use a message-code icon instead of a success-state checkmark.'
Assert-NotContains -Content $entry -Unexpected 'a01-icon-sms-code-v1.svg' -Message 'A01 SMS field must use the custom generated icon instead of the temporary third-party SVG.'
Assert-NotContains -Content $entry -Unexpected '@media (max-height:' -Message 'A01 must scroll naturally on short screens instead of compressing the selected design with height breakpoints.'
Assert-Contains -Content $entry -Expected 'import AppToast from "@/components/AppToast.vue";' -Message 'A01 feedback must consume the shared live-region Toast owner.'
Assert-NotContains -Content $entry -Unexpected 'class="feedback-toast"' -Message 'A01 must not duplicate the shared Toast implementation.'
Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'