Files
jiapuapp/tests/auth-accessibility-static-contract.ps1
T
2026-07-24 18:03:14 +08:00

135 lines
7.8 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
$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)<button\b$script:attributePattern>") |
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)<view\b$script:attributePattern>") |
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)<button\b$script:attributePattern>") |
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)<label\b[^>]*for=`"$([regex]::Escape($inputId))`"[^>]*>" "$Label label/for"
$input = [regex]::Matches($Content, "(?s)<input\b$script:attributePattern>") |
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)<text\b(?=[^>]*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)<AppToast\b(?=[^>]*:visible="feedbackVisible")(?=[^>]*:message="feedbackMessage")[^>]*/>' '认证状态播报实例'
if ($page.Contains('class="feedback-toast"')) { throw '认证页不得重复实现缺少稳定 live region 的 Toast' }
}
Require-Match $a01 '<view\s+class="login-tabs"\s+role="tablist"' 'A01 登录方式容器'
$a01Buttons = [regex]::Matches($a01, "(?s)<button\b$attributePattern>")
$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',
'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)<input\b$attributePattern>") |
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)<text\b[^>]*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)<button\b$attributePattern>")
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)<AppDialog\b(?=[^>]*: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'