Files
jiapuapp/tests/a01-a02-ui-contract.ps1
T
2026-07-16 18:13:03 +08:00

205 lines
10 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'
$legacyLoginPath = Join-Path $root 'pages/auth/a02-login.vue'
$pagesPath = Join-Path $root 'pages.json'
$globalStylesPath = Join-Path $root 'styles/global.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)."
}
$entry = Get-Content -LiteralPath $entryPath -Raw -Encoding utf8
$globalStyles = Get-Content -LiteralPath $globalStylesPath -Raw -Encoding utf8
# 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: 360rpx;' -Message 'A01 title divider must be wide enough to remain visible on paper.'
Assert-Contains -Content $dividerRule.Groups['Body'].Value -Expected 'height: 60rpx;' -Message 'A01 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($entry, '(?ms)^\.brand-seal\s*\{(?<Body>.*?)^\}')
if (-not $brandSealRule.Success) {
throw 'A01 is missing the brand seal style rule.'
}
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'width: 184rpx;' -Message 'A01 brand seal must use the approved smaller width.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'height: 221rpx;' -Message 'A01 brand seal must preserve its aspect ratio at the approved smaller height.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'top: 64rpx;' -Message 'A01 brand seal must keep its approved vertical anchor.'
Assert-Contains -Content $brandSealRule.Groups['Body'].Value -Expected 'left: 50%;' -Message 'A01 brand seal must remain horizontally centered.'
# Final decorative surfaces must come from real bitmap assets.
foreach ($asset in @(
'a01-red-hall-ink-backdrop-v1.png',
'brand-seal.png',
'a01-vnext-divider-v1.png',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png',
'a01-scroll-toast-v3.png',
'a01-scroll-dialog-v3.png',
'auth-wechat.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 -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 class="button-skin"[^>]+>')
if ($buttonSkinTags.Count -ne 2) {
throw "A01 must have exactly two visible login button skins, 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 cleaned transparent v2 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',
'5b6u5L+h55m75b2V',
'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 LOGIN_METHOD_STORAGE_KEY = 'a01:last-login-method'",
'uni.getStorageSync(LOGIN_METHOD_STORAGE_KEY)',
'uni.setStorageSync(LOGIN_METHOD_STORAGE_KEY, method)',
'const switchLoginMethod = (method) =>',
'const togglePasswordVisibility = () =>',
"url: '/pages/auth/a04-register'",
"url: '/pages/auth/a05-reset-password'",
'class="feedback-toast"',
'class="verification-layer"',
'class="login-submit"',
'class="agreement-error"',
'const agreementError = ref(false)'
)) {
Assert-Contains -Content $entry -Expected $contract -Message "A01 is missing state or interaction contract: $contract"
}
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-NotContains -Content $entry -Unexpected 'feedback-toast__skin' -Message 'A01 feedback Toast must use the dedicated nine-slice border asset instead of an absolutely stretched image.'
$feedbackToastRule = [regex]::Match($entry, '(?ms)^\.feedback-toast\s*\{(?<Body>.*?)^\}')
if (-not $feedbackToastRule.Success) {
throw 'A01 is missing the custom feedback Toast style rule.'
}
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected "border-image-source: url('/static/assets/foundation/transparent/a01-scroll-toast-v3.png');" -Message 'A01 feedback Toast must use the approved v3 nine-slice asset.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'border-image-slice:' -Message 'A01 feedback Toast must declare fixed decorative slices.'
Assert-Contains -Content $feedbackToastRule.Groups['Body'].Value -Expected 'fill' -Message 'A01 feedback Toast nine-slice must fill the paper center.'
Write-Output 'A01-LOGIN-MERGE-CONTRACT PASS'