$ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $attributePattern = '(?:"[^"]*"|''[^'']*''|[^>"'']+)*' function Read-ProjectFile { param([string]$Path) return Get-Content -LiteralPath (Join-Path $root $Path) -Raw -Encoding UTF8 } function Require-Match { param([string]$Content, [string]$Pattern, [string]$Label) if ($Content -notmatch $Pattern) { throw "$Label 未满足" } } function Require-ButtonClass { param([string]$Content, [string]$Class, [string]$Label) $button = [regex]::Matches($Content, "(?s)") | Where-Object { $_.Value -match "class=`"[^`"]*\b$([regex]::Escape($Class))\b" } | Select-Object -First 1 if ($null -eq $button) { throw "$Label 必须由原生 button 承载:$Class" } $view = [regex]::Matches($Content, "(?s)") | Where-Object { $_.Value -match "class=`"[^`"]*\b$([regex]::Escape($Class))\b" -and $_.Value.Contains('@click') } | Select-Object -First 1 if ($null -ne $view) { throw "$Label 不得继续用 view 模拟按钮:$Class" } } function Get-ButtonByClass { param([string]$Content, [string]$Class) return [regex]::Matches($Content, "(?s)") | Where-Object { $_.Value -match "class=`"[^`"]*\b$([regex]::Escape($Class))\b" } | Select-Object -First 1 } function Require-FieldRelation { param([string]$Content, [string]$Prefix, [string]$IdSuffix, [string]$ErrorKey, [string]$Label) $inputId = "$Prefix-$IdSuffix" $errorId = "$inputId-error" Require-Match $Content "(?s)]*for=`"$([regex]::Escape($inputId))`"[^>]*>" "$Label label/for" $input = [regex]::Matches($Content, "(?s)") | Where-Object { $_.Value.Contains("id=`"$inputId`"") } | Select-Object -First 1 if ($null -eq $input) { throw "$Label 缺少对应 input:$inputId" } foreach ($pattern in @( ":aria-invalid=`"Boolean\(fieldErrors\.$([regex]::Escape($ErrorKey))\)`"", ":aria-describedby=`"fieldErrors\.$([regex]::Escape($ErrorKey)) \? '$([regex]::Escape($errorId))' : undefined`"" )) { if ($input.Value -notmatch $pattern) { throw "$Label 输入错误关联缺失:$inputId" } } Require-Match $Content "(?s)]*id=`"$([regex]::Escape($errorId))`")(?=[^>]*role=`"alert`")[^>]*>" "$Label 错误节点" } $a01 = Read-ProjectFile 'pages/auth/a01-entry.vue' $a04 = Read-ProjectFile 'pages/auth/a04-register.vue' $a05 = Read-ProjectFile 'pages/auth/a05-reset-password.vue' $globalStyles = Read-ProjectFile 'styles/global.scss' foreach ($page in @($a01, $a04, $a05)) { Require-Match $page 'import\s+AppToast\s+from\s+["'']@/components/AppToast\.vue["'']' '认证状态播报 import' Require-Match $page '(?s)]*:visible="feedbackVisible")(?=[^>]*:message="feedbackMessage")[^>]*/>' '认证状态播报实例' if ($page.Contains('class="feedback-toast"')) { throw '认证页不得重复实现缺少稳定 live region 的 Toast' } } Require-Match $a01 '") $tabButtons = @($a01Buttons | Where-Object { $_.Value.Contains('role="tab"') }) if ($tabButtons.Count -ne 2) { throw 'A01 两个登录方式必须都是原生 tab 按钮' } if (@($tabButtons | Where-Object { $_.Value.Contains(':aria-selected=') }).Count -ne 2) { throw 'A01 两个 tab 都必须声明选中状态' } foreach ($class in @( 'login-tab', 'password-toggle', 'get-code', 'forgot-password', 'login-submit', 'wechat-login', 'register-link', 'agreement-toggle', 'agreement-link' )) { Require-ButtonClass $a01 $class 'A01' } if (@($a01Buttons | Where-Object { $_.Value -match 'class="[^"]*\bagreement-link\b' }).Count -ne 2) { throw 'A01 两份协议必须各自可聚焦' } foreach ($name in @('手机号', '登录密码', '短信验证码')) { $namedInput = [regex]::Matches($a01, "(?s)") | Where-Object { $_.Value.Contains("aria-label=`"$name`"") } | Select-Object -First 1 if ($null -eq $namedInput) { throw "A01 输入缺少名称:$name" } } $passwordToggle = Get-ButtonByClass $a01 'password-toggle' if ($passwordToggle.Value -notmatch ':aria-label="passwordVisible \? ''隐藏密码'' : ''显示密码''"' -or -not $passwordToggle.Value.Contains(':aria-pressed="passwordVisible"')) { throw 'A01 密码显隐状态未关联' } $a01GetCode = Get-ButtonByClass $a01 'get-code' if (-not $a01GetCode.Value.Contains(':disabled="sendingCode || submitting || cooldownSeconds > 0"')) { throw 'A01 短信按钮禁用态未关联' } $a01Submit = Get-ButtonByClass $a01 'login-submit' if (-not $a01Submit.Value.Contains(':disabled="submitting || sendingCode || tacVisible"') -or -not $a01Submit.Value.Contains(':aria-busy="submitting"')) { throw 'A01 提交忙碌态未关联' } $a01Agreement = Get-ButtonByClass $a01 'agreement-toggle' if (-not $a01Agreement.Value.Contains('role="checkbox"') -or -not $a01Agreement.Value.Contains(':aria-checked="agreed"')) { throw 'A01 协议复选语义未关联' } Require-Match $a01 '(?s)]*class="agreement-error"[^>]*role="alert"' 'A01 协议错误播报' foreach ($class in @('back-button', 'get-code', 'register-submit', 'agreement-toggle', 'agreement-link', 'login-entry__link')) { Require-ButtonClass $a04 $class 'A04' } $a04Buttons = [regex]::Matches($a04, "(?s)") if (@($a04Buttons | Where-Object { $_.Value -match 'class="[^"]*\bagreement-link\b' }).Count -ne 2) { throw 'A04 两份协议必须各自可聚焦' } foreach ($class in @('back-button', 'get-code', 'reset-submit', 'login-entry__link')) { Require-ButtonClass $a05 $class 'A05' } foreach ($entry in @( @{ Key = 'A04'; Content = $a04; Prefix = 'a04'; SubmitClass = 'register-submit' }, @{ Key = 'A05'; Content = $a05; Prefix = 'a05'; SubmitClass = 'reset-submit' } )) { Require-FieldRelation $entry.Content $entry.Prefix 'phone' 'phone' "$($entry.Key) 手机号" Require-FieldRelation $entry.Content $entry.Prefix 'verification-code' 'verificationCode' "$($entry.Key) 验证码" Require-FieldRelation $entry.Content $entry.Prefix 'password' 'password' "$($entry.Key) 密码" Require-FieldRelation $entry.Content $entry.Prefix 'confirm-password' 'confirmPassword' "$($entry.Key) 确认密码" $getCode = Get-ButtonByClass $entry.Content 'get-code' if ($entry.Key -eq 'A04') { $requiredDisabled = ':disabled="sendingCode || submitting || cooldownSeconds > 0 || registrationCommitted"' } else { $requiredDisabled = ':disabled="sendingCode || submitting || cooldownSeconds > 0"' } if (-not $getCode.Value.Contains($requiredDisabled)) { throw "$($entry.Key) 短信按钮禁用态未关联" } $submit = Get-ButtonByClass $entry.Content $entry.SubmitClass if (-not $submit.Value.Contains(':disabled="submitting || sendingCode || tacVisible"') -or -not $submit.Value.Contains(':aria-busy="submitting"')) { throw "$($entry.Key) 主提交忙碌态未关联" } } $a04Agreement = Get-ButtonByClass $a04 'agreement-toggle' if (-not $a04Agreement.Value.Contains('role="checkbox"') -or -not $a04Agreement.Value.Contains(':aria-checked="agreed"')) { throw 'A04 协议复选语义未关联' } Require-Match $a05 '(?s)]*:visible="successVisible")(?=[^>]*title="密码已重设")[^>]*>' 'A05 成功终态对话框' if ($a05.Contains('class="success-layer"')) { throw 'A05 成功终态必须复用唯一 AppDialog owner' } foreach ($token in @( '.auth-plain-button {', 'margin: 0;', 'padding: 0;', 'border: 0;', 'background: transparent;', 'line-height: normal;', '.auth-plain-button::after {', '.auth-plain-button:focus-visible {', '.auth-page.auth-page .auth-plain-button {', 'min-width: 48px;', 'min-height: 48px;' )) { if (-not $globalStyles.Contains($token)) { throw "认证原生按钮重置 owner 缺少:$token" } } Write-Output 'AUTH-ACCESSIBILITY-STATIC-CONTRACT PASS'