676 lines
57 KiB
PowerShell
676 lines
57 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
||
|
||
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 }
|
||
}
|
||
|
||
function Assert-Matches {
|
||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||
if ($Content -notmatch $Pattern) { throw $Message }
|
||
}
|
||
|
||
function Assert-Count {
|
||
param([string]$Content, [string]$Expected, [int]$Count, [string]$Message)
|
||
$actual = ([regex]::Matches($Content, [regex]::Escape($Expected))).Count
|
||
if ($actual -ne $Count) { throw "$Message,期望 $Count,实际 $actual" }
|
||
}
|
||
|
||
$root = Split-Path -Parent $PSScriptRoot
|
||
$a01 = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a01-entry.vue') -Raw -Encoding utf8
|
||
$a04 = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a04-register.vue') -Raw -Encoding utf8
|
||
$a05 = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a05-reset-password.vue') -Raw -Encoding utf8
|
||
$a06 = Get-Content -LiteralPath (Join-Path $root 'pages/auth/a06-auth-status.vue') -Raw -Encoding utf8
|
||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||
$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
|
||
$g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-overview.vue') -Raw -Encoding utf8
|
||
$g06 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g06-search-genealogies.vue') -Raw -Encoding utf8
|
||
$g08 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g08-join-application.vue') -Raw -Encoding utf8
|
||
$g09 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g09-my-applications.vue') -Raw -Encoding utf8
|
||
$g10 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g10-application-review.vue') -Raw -Encoding utf8
|
||
$g11 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g11-genealogy-settings.vue') -Raw -Encoding utf8
|
||
$g12 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g12-generation-poems.vue') -Raw -Encoding utf8
|
||
$t01 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t01-tree-overview.vue') -Raw -Encoding utf8
|
||
$t03 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile.vue') -Raw -Encoding utf8
|
||
$t04 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t04-add-relative.vue') -Raw -Encoding utf8
|
||
$t05 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t05-edit-member.vue') -Raw -Encoding utf8
|
||
$t06 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t06-edit-relationship.vue') -Raw -Encoding utf8
|
||
$t07 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
|
||
$t08 = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t08-member-states.vue') -Raw -Encoding utf8
|
||
$f01 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
|
||
$f02 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f02-publish-feed.vue') -Raw -Encoding utf8
|
||
$f03 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f03-feed-detail.vue') -Raw -Encoding utf8
|
||
$f04 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f04-article-list.vue') -Raw -Encoding utf8
|
||
$f05 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f05-article-detail.vue') -Raw -Encoding utf8
|
||
$f06 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f06-article-editor.vue') -Raw -Encoding utf8
|
||
$f07 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f07-album-list.vue') -Raw -Encoding utf8
|
||
$f08 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f08-album-detail.vue') -Raw -Encoding utf8
|
||
$f09 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f09-media-upload.vue') -Raw -Encoding utf8
|
||
$f10 = Get-Content -LiteralPath (Join-Path $root 'pages/family/f10-video-list.vue') -Raw -Encoding utf8
|
||
$r01 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r01-people-list.vue') -Raw -Encoding utf8
|
||
$r02 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r02-person-detail.vue') -Raw -Encoding utf8
|
||
$r03 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r03-gift-list.vue') -Raw -Encoding utf8
|
||
$r04 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r04-gift-editor.vue') -Raw -Encoding utf8
|
||
$r05 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r05-ritual-list.vue') -Raw -Encoding utf8
|
||
$r06 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r06-ritual-detail.vue') -Raw -Encoding utf8
|
||
$r07 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r07-ritual-editor.vue') -Raw -Encoding utf8
|
||
$r08 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r08-growth-journal.vue') -Raw -Encoding utf8
|
||
$r09 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r09-life-events.vue') -Raw -Encoding utf8
|
||
$r10 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r10-memo-list.vue') -Raw -Encoding utf8
|
||
$r11 = Get-Content -LiteralPath (Join-Path $root 'pages/records/r11-merit-records.vue') -Raw -Encoding utf8
|
||
$n01 = Get-Content -LiteralPath (Join-Path $root 'pages/notification/n01-message-center.vue') -Raw -Encoding utf8
|
||
$n02 = Get-Content -LiteralPath (Join-Path $root 'pages/notification/n02-message-detail.vue') -Raw -Encoding utf8
|
||
$m01 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m01-profile-home.vue') -Raw -Encoding utf8
|
||
$m02 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m02-edit-profile.vue') -Raw -Encoding utf8
|
||
$m03 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m03-security-settings.vue') -Raw -Encoding utf8
|
||
$m04 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m04-change-password.vue') -Raw -Encoding utf8
|
||
$m05 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m05-change-phone.vue') -Raw -Encoding utf8
|
||
$m06 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m06-help-center.vue') -Raw -Encoding utf8
|
||
$m07 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m07-feedback.vue') -Raw -Encoding utf8
|
||
$m08 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m08-promotion.vue') -Raw -Encoding utf8
|
||
$m09 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m09-vip-orders.vue') -Raw -Encoding utf8
|
||
$m10 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m10-about-settings.vue') -Raw -Encoding utf8
|
||
$mock = Get-Content -LiteralPath (Join-Path $root 'data/mock.js') -Raw -Encoding utf8
|
||
$navigation = Get-Content -LiteralPath (Join-Path $root 'utils/navigation.js') -Raw -Encoding utf8
|
||
$routes = Get-Content -LiteralPath (Join-Path $root 'utils/navigation-routes.js') -Raw -Encoding utf8
|
||
|
||
foreach ($page in @(
|
||
@{ Key = 'A01'; Content = $a01 },
|
||
@{ Key = 'A04'; Content = $a04 },
|
||
@{ Key = 'A05'; Content = $a05 },
|
||
@{ Key = 'A06'; Content = $a06 }
|
||
)) {
|
||
foreach ($forbidden in @(
|
||
'uni.navigateTo(',
|
||
'uni.navigateBack(',
|
||
'uni.redirectTo(',
|
||
'uni.reLaunch(',
|
||
'getCurrentPages(',
|
||
'<navigator',
|
||
'/pages/'
|
||
)) {
|
||
Assert-NotContains -Content $page.Content -Unexpected $forbidden -Message "$($page.Key) 不得绕过导航网关:$forbidden"
|
||
}
|
||
Assert-Matches -Content $page.Content -Pattern 'onBackPress\(\(event\)\s*=>' -Message "$($page.Key) 必须接管 Android 返回入口"
|
||
Assert-Contains -Content $page.Content -Expected 'handleBackPress(event, requestBack)' -Message "$($page.Key) 必须通过同步适配器处理 Android 返回"
|
||
}
|
||
|
||
foreach ($page in @(
|
||
@{ Key = 'A04'; Content = $a04 },
|
||
@{ Key = 'A05'; Content = $a05 },
|
||
@{ Key = 'A06'; Content = $a06 }
|
||
)) {
|
||
Assert-Contains -Content $page.Content -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message "$($page.Key) 必须把同步布尔结果原样返回平台"
|
||
}
|
||
|
||
Assert-Contains -Content $a01 -Expected 'openPage("A05", {}, "A01")' -Message 'A01 忘记密码必须声明 A05 与 A01 来源'
|
||
Assert-Contains -Content $a01 -Expected 'openPage("A04", {}, "A01")' -Message 'A01 注册入口必须声明 A04 与 A01 来源'
|
||
Assert-Matches -Content $a01 -Pattern '(?s)const requestBack = \(\) =>\s*runBackGuard\(\{\s*transientOpen: tacVisible\.value,\s*submitting: submitting\.value \|\| sendingCode\.value,\s*"close-transient": closeTac,\s*"block-submitting": \(\) => \{\s*cancelPendingRequest\(\);\s*return true;\s*\},\s*\}\);' -Message 'A01 返回守卫必须先关闭真实 TAC,并允许显式取消认证请求'
|
||
Assert-Matches -Content $a01 -Pattern '(?s)onBackPress\(\(event\) => \{\s*if \(!tacVisible\.value && \(submitting\.value \|\| sendingCode\.value\)\) \{\s*cancelPendingRequest\(\);\s*return false;\s*\}\s*if \(!tacVisible\.value\) return false;\s*return handleBackPress\(event, requestBack\);\s*\}\);' -Message 'A01 Android 返回必须先取消认证请求,再把根页退出交还平台'
|
||
|
||
Assert-Contains -Content $a04 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'A04 必须使用项目对话框确认放弃表单'
|
||
Assert-Contains -Content $a04 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'A04 必须消费唯一放弃确认控制器'
|
||
Assert-Contains -Content $a04 -Expected 'const isDirty = computed(() =>' -Message 'A04 必须计算未保存表单状态'
|
||
Assert-Matches -Content $a04 -Pattern '(?s)const requestBack = \(\) =>\s*registrationCommitted\.value\s*\? enterAuthenticatedRoot\(\)\s*:\s*runBackGuard\(\{\s*transientOpen: tacVisible\.value \|\| discardVisible\.value,\s*submitting: submitting\.value \|\| sendingCode\.value,\s*dirty: isDirty\.value,\s*"close-transient": tacVisible\.value \? closeTac : cancelDiscard,\s*"block-submitting": \(\) => \{\s*cancelPendingRequest\(\);\s*return requestBack\(\);\s*\},\s*"confirm-discard": requestDiscardConfirmation,\s*\}\);' -Message 'A04 requestBack 必须先处理已提交状态、再优先关闭 TAC;请求中返回先中止网络,再恢复脏表单守卫'
|
||
Assert-Contains -Content $a04 -Expected 'discardConfirmation.dispose();' -Message 'A04 卸载时必须通过唯一控制器释放等待者'
|
||
Assert-Count -Content $a04 -Expected '@click="requestBack"' -Count 2 -Message 'A04 页头和已有账号入口必须共用 requestBack'
|
||
Assert-Count -Content $a04 -Expected '<AppDialog' -Count 1 -Message 'A04 只允许一个放弃确认框'
|
||
|
||
Assert-Contains -Content $a05 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'A05 必须使用项目对话框确认放弃表单'
|
||
Assert-Contains -Content $a05 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'A05 必须消费唯一放弃确认控制器'
|
||
Assert-Contains -Content $a05 -Expected 'const isDirty = computed(() =>' -Message 'A05 必须计算未保存表单状态'
|
||
Assert-Matches -Content $a05 -Pattern '(?s)const requestBack = \(\) => \{\s*if \(successVisible\.value\) return leaveResetSuccess\(\);\s*return runBackGuard\(\{\s*transientOpen: tacVisible\.value \|\| discardVisible\.value,\s*submitting: submitting\.value \|\| sendingCode\.value,\s*dirty: isDirty\.value,\s*"close-transient": tacVisible\.value \? closeTac : cancelDiscard,\s*"block-submitting": \(\) => \{\s*cancelPendingRequest\(\);\s*return requestBack\(\);\s*\},\s*"confirm-discard": requestDiscardConfirmation,\s*\}\);\s*\};' -Message 'A05 requestBack 必须先处理成功终态;请求中返回先中止网络,再恢复放弃守卫'
|
||
Assert-Contains -Content $a05 -Expected 'discardConfirmation.dispose();' -Message 'A05 卸载时必须通过唯一控制器释放等待者'
|
||
Assert-Count -Content $a05 -Expected '@click="requestBack"' -Count 2 -Message 'A05 页头和想起密码入口必须共用 requestBack'
|
||
Assert-Count -Content $a05 -Expected '<AppDialog' -Count 2 -Message 'A05 必须分别复用一个放弃确认框和一个成功终态框'
|
||
Assert-Contains -Content $a05 -Expected 'const leaveResetSuccess = () => returnTo("A01", {});' -Message 'A05 远端成功终态必须无结果返回 A01'
|
||
Assert-Contains -Content $a05 -Expected 'if (successVisible.value) return leaveResetSuccess();' -Message 'A05 Android 返回必须从成功终态退出,不能重新暴露已提交表单'
|
||
Assert-Contains -Content $a05 -Expected '@confirm="leaveResetSuccess"' -Message 'A05 成功对话框 CTA 必须复用唯一成功终态出口'
|
||
Assert-Count -Content $a05 -Expected 'returnTo("A01", {})' -Count 1 -Message 'A05 无结果成功终点只能定义一次'
|
||
Assert-NotContains -Content $a05 -Unexpected 'finishPage(' -Message 'A05 重设成功不应发出跨页业务结果'
|
||
Assert-NotContains -Content $a05 -Unexpected 'password-reset' -Message 'A05 不得伪造 password-reset 导航结果'
|
||
|
||
foreach ($form in @(
|
||
@{ Key = 'A04'; Content = $a04; NonMaskDialogCount = 1 },
|
||
@{ Key = 'A05'; Content = $a05; NonMaskDialogCount = 2 }
|
||
)) {
|
||
Assert-Matches -Content $form.Content -Pattern '(?s)const discardConfirmation = createDiscardConfirmation\(\(visible\) => \{\s*discardVisible\.value = visible;\s*\}\);' -Message "$($form.Key) 必须把对话框可见状态交给唯一控制器"
|
||
foreach ($binding in @(
|
||
'const requestDiscardConfirmation = discardConfirmation.request;',
|
||
'const confirmDiscard = discardConfirmation.confirm;',
|
||
'const cancelDiscard = discardConfirmation.cancel;'
|
||
)) {
|
||
Assert-Contains -Content $form.Content -Expected $binding -Message "$($form.Key) 缺少唯一控制器绑定:$binding"
|
||
}
|
||
foreach ($duplicateOwner in @('let discardPromise', 'resolveDiscardConfirmation', 'settleDiscardConfirmation')) {
|
||
Assert-NotContains -Content $form.Content -Unexpected $duplicateOwner -Message "$($form.Key) 不得保留页面级放弃确认 owner:$duplicateOwner"
|
||
}
|
||
Assert-Count -Content $form.Content -Expected ':close-on-mask="false"' -Count $form.NonMaskDialogCount -Message "$($form.Key) 的确认与成功终态框不得被遮罩点击隐式关闭"
|
||
Assert-Count -Content $form.Content -Expected '@confirm="confirmDiscard"' -Count 1 -Message "$($form.Key) 放弃确认只能有一个确认入口"
|
||
Assert-Count -Content $form.Content -Expected '@cancel="cancelDiscard"' -Count 1 -Message "$($form.Key) 放弃确认只能有一个取消入口"
|
||
}
|
||
|
||
Assert-Contains -Content $a06 -Expected 'const resolveStatus = (options = {}) =>' -Message 'A06 必须直接消费 onLoad 页面参数'
|
||
Assert-Contains -Content $a06 -Expected 'options.status || "risk"' -Message 'A06 必须从 onLoad 参数解析状态并安全回退'
|
||
Assert-NotContains -Content $a06 -Unexpected 'URLSearchParams' -Message 'A06 不得保留浏览器地址解析旁路'
|
||
Assert-NotContains -Content $a06 -Unexpected 'onShow(' -Message 'A06 不得在 onShow 重读已丢失的路由参数'
|
||
Assert-Matches -Content $a06 -Pattern '(?s)const requestBack = \(\) => \{\s*if \(recoveryVisible\.value\) \{\s*return runBackGuard\(\{\s*transientOpen: true,\s*"close-transient": closeRecovery,\s*\}\);\s*\}\s*return goRoot\("A01"\);\s*\};' -Message 'A06 必须先通过守卫关闭恢复浮层,否则回到唯一认证根页'
|
||
Assert-Count -Content $a06 -Expected '@click="requestBack"' -Count 2 -Message 'A06 页头和返回登录入口必须共用 requestBack'
|
||
|
||
foreach ($page in @(
|
||
@{ Key = 'G01'; Content = $g01 },
|
||
@{ Key = 'G03'; Content = $g03 },
|
||
@{ Key = 'G05'; Content = $g05 },
|
||
@{ Key = 'G06'; Content = $g06 },
|
||
@{ Key = 'G08'; Content = $g08 },
|
||
@{ Key = 'G09'; Content = $g09 },
|
||
@{ Key = 'G10'; Content = $g10 },
|
||
@{ Key = 'G11'; Content = $g11 },
|
||
@{ Key = 'G12'; Content = $g12 }
|
||
)) {
|
||
foreach ($forbidden in @(
|
||
'uni.navigateTo(',
|
||
'uni.navigateBack(',
|
||
'uni.redirectTo(',
|
||
'uni.reLaunch(',
|
||
'getCurrentPages(',
|
||
'<navigator',
|
||
'/pages/'
|
||
)) {
|
||
Assert-NotContains -Content $page.Content -Unexpected $forbidden -Message "$($page.Key) 不得绕过导航网关:$forbidden"
|
||
}
|
||
}
|
||
|
||
Assert-Contains -Content $g01 -Expected 'import { handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";' -Message 'G01 必须只消费导航网关与根页返回适配器'
|
||
Assert-NotContains -Content $g01 -Unexpected 'Number(query?.genealogyId)' -Message 'G01 不得把可能为 int64 的家谱 ID 转成 Number'
|
||
Assert-Contains -Content $g01 -Expected 'requestedGenealogyId.value = String(query?.genealogyId || "");' -Message 'G01 路由家谱 ID 必须保持词法字符串'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)const openGenealogy = \(genealogy\) =>\s*openPage\("G05", \{ genealogyId: String\(genealogy\.id\) \}, "G01"\)\.then\(\s*\(opened\) => \{\s*if \(opened\) \{\s*selectedGenealogyId\.value = String\(genealogy\.id\);\s*genealogyContext\.setCurrentGenealogyId\(genealogy\.id\);\s*\}\s*return opened;\s*\},\s*\);' -Message 'G01 只有导航成功后才能同步页面与持久 context,避免并发点击串谱'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)const openShortcut = \(key\) => \{(?:(?!setCurrentGenealogyId).)*const actions =' -Message 'G01 快捷入口不得在导航结果确定前改写当前家谱 context'
|
||
Assert-Contains -Content $g01 -Expected 'openPage("G03", {}, "G01")' -Message 'G01 创建入口必须进入 G03'
|
||
Assert-Contains -Content $g01 -Expected 'openPage("G06", {}, "G01")' -Message 'G01 搜索入口必须进入 G06'
|
||
Assert-Contains -Content $g01 -Expected 'openPage("G06", { mode: "invite" }, "G01")' -Message 'G01 邀请码入口必须进入 G06 邀请模式'
|
||
Assert-Contains -Content $g01 -Expected 'openPage("N01", notificationParams, "G01")' -Message 'G01 通知入口必须声明 N01 与当前家谱上下文'
|
||
foreach ($mapping in @(
|
||
'tree: () => openPage("T01", { genealogyId }, "G01")',
|
||
'members: () => openPage("G05", { genealogyId }, "G01")',
|
||
'poem: () => openPage("G12", { genealogyId }, "G01")',
|
||
'applications: () => openPage("G10", { genealogyId }, "G01")'
|
||
)) {
|
||
Assert-Contains -Content $g01 -Expected $mapping -Message "G01 快捷入口缺少规范映射:$mapping"
|
||
}
|
||
Assert-Contains -Content $g01 -Expected 'openPage("G09", { status: "pending" }, "G01")' -Message 'G01 审核中记录必须进入 G09'
|
||
Assert-Contains -Content $g01 -Expected 'openPage(' -Message 'G01 必须通过导航网关进入申请流程'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)return openPage\(\s*"G08",\s*\{\s*genealogyId: String\(record\.genealogyId\),\s*source: "search",\s*\},\s*"G01",\s*\);' -Message 'G01 被拒绝或已退出记录必须携带字符串家谱 ID 进入 G08'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)const createGenealogy = \(\) => \{\s*closeAddDialog\(\);\s*return openPage\("G03", \{\}, "G01"\);\s*\};' -Message 'G01 添加面板创建入口必须先关闭面板再跳转'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)const requestBack = \(\) =>\s*runBackGuard\(\{\s*transientOpen: switcherVisible\.value \|\| addDialogVisible\.value,\s*"close-transient": closeActiveOverlay,\s*\}\);' -Message 'G01 返回守卫必须只关闭当前浮层'
|
||
Assert-Matches -Content $g01 -Pattern '(?s)onBackPress\(\(event\) => \{\s*if \(!switcherVisible\.value && !addDialogVisible\.value\) return false;\s*return handleBackPress\(event, requestBack\);\s*\}\);' -Message 'G01 无浮层时必须把根页 Android 返回交还平台'
|
||
|
||
Assert-Contains -Content $g03 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'G03 脏表单必须使用项目对话框'
|
||
Assert-Contains -Content $g03 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'G03 必须消费唯一放弃确认控制器'
|
||
Assert-Contains -Content $g03 -Expected 'import { handleBackPress, openPage, returnTo, runBackGuard } from "@/utils/navigation.js";' -Message 'G03 必须只消费导航网关和返回守卫'
|
||
Assert-Contains -Content $g03 -Expected '@back="requestBack"' -Message 'G03 页头必须复用统一返回守卫'
|
||
Assert-Contains -Content $g03 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G03 Android 返回必须使用同步适配器'
|
||
Assert-Contains -Content $g03 -Expected 'const isDirty = computed(() =>' -Message 'G03 必须识别未保存表单'
|
||
Assert-Contains -Content $g03 -Expected 'internalTrail: isAncestorStep.value' -Message 'G03 首代步骤必须作为同页内部轨迹返回'
|
||
Assert-Contains -Content $g03 -Expected 'submitting: isSubmitting.value' -Message 'G03 提交期间必须阻止离开'
|
||
Assert-Contains -Content $g03 -Expected 'transientOpen: duplicateReminderVisible.value || discardVisible.value' -Message 'G03 返回必须先处理重复提醒或放弃确认层'
|
||
Assert-Contains -Content $g03 -Expected 'currentStep.value = "ancestor";' -Message 'G03 创建预览成功后必须在同页进入首代步骤'
|
||
Assert-Matches -Content $g03 -Pattern '(?s)const confirmCreate = \(\) => \{\s*if \(isSubmitting\.value\) return;\s*closeDuplicateReminder\(\);' -Message 'G03 重复确认必须在创建定时器产生前被原子阻止'
|
||
Assert-Contains -Content $g03 -Expected 'const createSnapshot = Object.freeze({ ...createForm });' -Message 'G03 创建校验通过后必须快照本次提交值'
|
||
Assert-Contains -Content $g03 -Expected 'const ancestorSnapshot = Object.freeze({ ...ancestorForm });' -Message 'G03 首代校验通过后必须快照本次提交值'
|
||
Assert-Contains -Content $g03 -Expected 'if (submitTimer !== timer) return;' -Message 'G03 定时器回调必须校验自己仍是当前提交'
|
||
Assert-Contains -Content $g03 -Expected 'openPage("G06", {}, "G03")' -Message 'G03 搜索已有家谱必须通过网关'
|
||
Assert-Contains -Content $g03 -Expected 'const enterOverview = () =>' -Message 'G03 必须定义唯一成功终态出口'
|
||
Assert-Contains -Content $g03 -Expected 'returnTo("G05", { genealogyId: genealogyId.value });' -Message 'G03 本地成功终态必须无结果进入 G05'
|
||
Assert-Count -Content $g03 -Expected 'returnTo("G05", { genealogyId: genealogyId.value })' -Count 1 -Message 'G03 无结果成功终点只能定义一次'
|
||
Assert-Contains -Content $g03 -Expected 'discardConfirmation.dispose();' -Message 'G03 卸载时必须释放放弃确认等待者'
|
||
foreach ($forbidden in @('query?.step', 'step=ancestor', 'finishPage(', 'genealogy-created', 'application-created')) {
|
||
Assert-NotContains -Content $g03 -Unexpected $forbidden -Message "G03 本地预览不得保留旧流程或伪造后端成功:$forbidden"
|
||
}
|
||
Assert-NotContains -Content $g03 -Unexpected 'onLoad(' -Message 'G03 没有路由业务参数,不得保留无语义的 onLoad 兼容壳'
|
||
|
||
Assert-Contains -Content $g05 -Expected 'appApi.getOverview(genealogyId.value' -Message 'G05 必须消费真实家谱概览 owner'
|
||
Assert-Matches -Content $g05 -Pattern '(?s)import \{\s*goBack,\s*goRoot,\s*handleBackPress,\s*openPage,\s*returnTo,\s*\} from "@/utils/navigation\.js";' -Message 'G05 必须只消费导航网关与 Android 返回适配器'
|
||
Assert-Contains -Content $g05 -Expected '@back="requestBack"' -Message 'G05 页头必须使用统一返回入口'
|
||
Assert-Contains -Content $g05 -Expected 'const requestBack = () => goBack();' -Message 'G05 返回必须保留真实栈并支持深链父页回退'
|
||
Assert-Contains -Content $g05 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G05 Android 返回必须使用同步适配器'
|
||
Assert-Contains -Content $g05 -Expected 'const accessRole = ref("guest");' -Message 'G05 未验证访问默认必须是最低权限'
|
||
Assert-Contains -Content $g05 -Expected 'accessRole.value = result.accessRole;' -Message 'G05 显示角色必须来自概览窄映射'
|
||
Assert-NotContains -Content $g05 -Unexpected 'findGenealogyFixture' -Message 'G05 不得继续读取本地家谱夹具'
|
||
Assert-Contains -Content $g05 -Expected 'overviewState.value = "no-permission";' -Message 'G05 未知家谱必须明确拒绝访问'
|
||
Assert-Contains -Content $g05 -Expected 'returnTo("G01", {})' -Message 'G05 状态页显式按钮必须返回家谱根页'
|
||
foreach ($mapping in @(
|
||
'openPage("T01", { genealogyId: genealogyId.value }, "G05")',
|
||
'goRoot("F01", { genealogyId: genealogyId.value })',
|
||
'openPage("G10", { genealogyId: genealogyId.value }, "G05")',
|
||
'openPage("G11", { genealogyId: genealogyId.value }, "G05")',
|
||
'openPage("G12", { genealogyId: genealogyId.value }, "G05")'
|
||
)) {
|
||
Assert-Contains -Content $g05 -Expected $mapping -Message "G05 缺少规范入口:$mapping"
|
||
}
|
||
Assert-Matches -Content $g05 -Pattern '(?s)openPage\(\s*"G08",\s*\{ genealogyId: genealogyId\.value, source: "search" \},\s*"G05",\s*\)' -Message 'G05 公开申请必须通过 G08 规范入口'
|
||
Assert-Contains -Content $g05 -Expected 'openPage("G09", { status: "pending" }, "G05")' -Message 'G05 审核中关系必须进入 G09 而不是重复申请'
|
||
foreach ($forbidden in @('query.mode', 'query.role', 'query.genealogyName', 'toFirstPerson', 'finishPage(', 'genealogy-created')) {
|
||
Assert-NotContains -Content $g05 -Unexpected $forbidden -Message "G05 不得信任查询权限或保留重复流程:$forbidden"
|
||
}
|
||
Assert-Matches -Content $routes -Pattern '(?s)G08:\s*defineRoute\(\{.*?allowedSources:\s*\["G01", "G05", "G06", "G09"\]' -Message 'G08 必须允许 G01 的被拒绝与已退出申请进入'
|
||
|
||
Assert-Contains -Content $g06 -Expected 'import { goRoot, openPage } from "@/utils/navigation.js";' -Message 'G06 必须只消费导航网关'
|
||
Assert-Matches -Content $g06 -Pattern '(?s)openPage\(\s*"G05",\s*\{ genealogyId: String\(item\.id\) \},\s*"G06",\s*\)' -Message 'G06 公开预览必须只携带字符串家谱 ID 进入 G05'
|
||
Assert-Matches -Content $g06 -Pattern '(?s)openPage\(\s*"G08",\s*\{ genealogyId: String\(item\.id\), source: "search" \},\s*"G06",\s*\)' -Message 'G06 搜索申请必须进入 G08'
|
||
Assert-Contains -Content $g06 -Expected 'openPage("G09", { status: "pending" }, "G06")' -Message 'G06 审核中结果必须进入 G09'
|
||
Assert-Matches -Content $g06 -Pattern '(?s)goRoot\("G01", \{ genealogyId: String\(item\.id\) \}\)' -Message 'G06 已加入或自建家谱必须回到 G01 根语义'
|
||
Assert-Matches -Content $g06 -Pattern '(?s)openPage\(\s*"G08",\s*\{\s*genealogyId: String\(inviteTarget\.value\.id\),\s*source: "invite",\s*\},\s*"G06",\s*\)' -Message 'G06 邀请码预览必须进入 G08,不能本地直接入谱'
|
||
foreach ($forbidden in @('genealogyName:', 'previous:', 'mode: "public"', 'finishPage(', 'application-created')) {
|
||
Assert-NotContains -Content $g06 -Unexpected $forbidden -Message "G06 不得传播未声明业务状态或伪成功:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $g08 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'G08 脏表单必须使用项目对话框'
|
||
Assert-Matches -Content $g08 -Pattern '(?s)import \{\s*findGenealogyFixture,\s*getGenealogyFixtureAccess,\s*\} from "@/data/mock\.js";' -Message 'G08 名称与申请资格必须来自共享夹具 owner'
|
||
Assert-Contains -Content $g08 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'G08 必须消费唯一放弃确认控制器'
|
||
foreach ($navigationSymbol in @('goBack', 'goRoot', 'handleBackPress', 'openPage', 'returnTo', 'runBackGuard')) {
|
||
Assert-Contains -Content $g08 -Expected $navigationSymbol -Message "G08 缺少统一导航语义:$navigationSymbol"
|
||
}
|
||
Assert-Contains -Content $g08 -Expected '<PageHeader :title="sourceContract.headerTitle" custom-back @back="requestBack" />' -Message 'G08 页头必须复用统一返回守卫'
|
||
Assert-Contains -Content $g08 -Expected 'const genealogyId = ref("");' -Message 'G08 必须持有词法家谱 ID'
|
||
Assert-Contains -Content $g08 -Expected 'genealogyId.value = String(query.genealogyId || "");' -Message 'G08 路由家谱 ID 必须保持字符串'
|
||
Assert-Contains -Content $g08 -Expected 'const fixture = findGenealogyFixture(genealogyId.value);' -Message 'G08 必须从共享 owner 读取家谱名称'
|
||
Assert-Contains -Content $g08 -Expected 'const access = getGenealogyFixtureAccess(genealogyId.value);' -Message 'G08 必须从共享 owner 读取申请资格'
|
||
Assert-Contains -Content $g08 -Expected 'if (!fixture || !access.canApply)' -Message 'G08 未知、已加入、所有者与审核中关系必须失败关闭'
|
||
Assert-Contains -Content $g08 -Expected 'const isDirty = computed(() =>' -Message 'G08 必须识别未保存表单'
|
||
Assert-Contains -Content $g08 -Expected 'submitting: isSubmitting.value' -Message 'G08 提交期间必须阻止离开'
|
||
Assert-Contains -Content $g08 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G08 Android 返回必须使用同步适配器'
|
||
Assert-Contains -Content $g08 -Expected 'discardConfirmation.dispose();' -Message 'G08 卸载必须释放确认等待者'
|
||
Assert-Matches -Content $g08 -Pattern '(?s)onUnload\(\(\) => \{\s*const timer = submitTimer;\s*submitTimer = null;\s*if \(timer\) clearTimeout\(timer\);\s*discardConfirmation\.dispose\(\);\s*\}\);' -Message 'G08 卸载必须先作废再清理本地预览定时器与确认等待者'
|
||
Assert-Contains -Content $g08 -Expected 'const submitSnapshot = Object.freeze({ ...form });' -Message 'G08 本地校验必须快照本次提交值'
|
||
Assert-Contains -Content $g08 -Expected 'if (submitTimer !== timer) return;' -Message 'G08 定时器回调必须校验自己仍是当前提交'
|
||
Assert-Contains -Content $g08 -Expected 'returnTo("G09", {})' -Message 'G08 搜索申请本地成功只能无结果进入 G09'
|
||
Assert-Contains -Content $g08 -Expected 'returnTo("G01", {})' -Message 'G08 邀请码本地成功不得选中新家谱'
|
||
Assert-Contains -Content $g08 -Expected '本地流程预览' -Message 'G08 必须明确本地成功尚未提交服务器'
|
||
foreach ($forbidden in @('query.previous', 'query.genealogyName', 'previousRelation', 'decodeURIComponent', 'genealogyPreview', 'finishPage(', 'application-created', '已直接加入', '无需等待审核')) {
|
||
Assert-NotContains -Content $g08 -Unexpected $forbidden -Message "G08 不得消费旧查询字段或伪造后端成功:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $g09 -Expected 'import { handleBackPress, openPage, runBackGuard } from "@/utils/navigation.js";' -Message 'G09 必须只消费导航网关与浮层返回守卫'
|
||
Assert-Contains -Content $g09 -Expected '<PageHeader title="我的申请" custom-back @back="requestBack" />' -Message 'G09 页头必须复用浮层返回守卫'
|
||
Assert-Contains -Content $g09 -Expected ':close-on-mask="false"' -Message 'G09 撤回确认不得由遮罩隐式关闭'
|
||
Assert-Count -Content $g09 -Expected '@cancel="cancelWithdraw"' -Count 1 -Message 'G09 撤回取消只能有一个入口'
|
||
Assert-NotContains -Content $g09 -Unexpected '@close="cancelWithdraw"' -Message 'G09 不得由 cancel 与 close 重复处理同一次关闭'
|
||
Assert-Contains -Content $g09 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G09 Android 返回必须先关闭撤回确认层'
|
||
Assert-Contains -Content $g09 -Expected 'LOCAL_WITHDRAWN' -Message 'G09 本地撤回必须使用非领域预览状态'
|
||
Assert-Contains -Content $g09 -Expected '尚未提交服务器' -Message 'G09 本地撤回必须明确不改变服务器申请'
|
||
Assert-NotContains -Content $g09 -Unexpected 'target.status = "WITHDRAWN"' -Message 'G09 不得把本地预览写成真实撤回状态'
|
||
Assert-Matches -Content $g09 -Pattern '(?s)openPage\(\s*"G05",\s*\{ genealogyId: String\(item\.genealogyId\) \},\s*"G09",\s*\)' -Message 'G09 已通过申请必须进入可信家谱总览'
|
||
Assert-Matches -Content $g09 -Pattern '(?s)openPage\(\s*"G08",\s*\{ genealogyId: String\(item\.genealogyId\), source: "search" \},\s*"G09",\s*\)' -Message 'G09 重新申请必须只携带注册字段进入 G08'
|
||
Assert-Contains -Content $g09 -Expected 'openPage("G06", {}, "G09")' -Message 'G09 搜索入口必须进入 G06'
|
||
foreach ($forbidden in @('previous:', 'finishPage(', 'application-created')) {
|
||
Assert-NotContains -Content $g09 -Unexpected $forbidden -Message "G09 本地状态不得传播旧字段或结果:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $g10 -Expected 'import { getGenealogyFixtureAccess } from "@/data/mock.js";' -Message 'G10 审核权限必须来自共享夹具解析器'
|
||
Assert-Contains -Content $g10 -Expected 'import { handleBackPress, runBackGuard } from "@/utils/navigation.js";' -Message 'G10 必须使用统一浮层返回守卫'
|
||
Assert-Contains -Content $g10 -Expected 'genealogyId.value = String(query.genealogyId || "");' -Message 'G10 家谱 ID 必须保持词法字符串'
|
||
Assert-Contains -Content $g10 -Expected 'getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"' -Message 'G10 非 owner 必须失败关闭'
|
||
Assert-Contains -Content $g10 -Expected '@back="requestBack"' -Message 'G10 页头必须复用返回守卫'
|
||
Assert-Contains -Content $g10 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G10 Android 返回必须先关闭审核或说明弹层'
|
||
Assert-Contains -Content $g10 -Expected ':close-on-mask="false"' -Message 'G10 审核弹层不得由遮罩隐式关闭'
|
||
Assert-Count -Content $g10 -Expected '@cancel="closeDialog"' -Count 1 -Message 'G10 弹层取消只能有一个入口'
|
||
Assert-NotContains -Content $g10 -Unexpected '@close="closeDialog"' -Message 'G10 不得重复处理 cancel 与 close'
|
||
Assert-Contains -Content $g10 -Expected '尚未提交服务器' -Message 'G10 本地审核必须明确未提交服务器'
|
||
foreach ($forbidden in @('query.genealogyId?.value', 'genealogyContext', 'finishPage(', 'application-reviewed')) {
|
||
Assert-NotContains -Content $g10 -Unexpected $forbidden -Message "G10 不得保留兼容读取、上下文旁路或本地结果:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $g11 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'G11 脏表单必须使用项目对话框'
|
||
Assert-Matches -Content $g11 -Pattern '(?s)import \{\s*findGenealogyFixture,\s*getGenealogyFixtureAccess,\s*\} from "@/data/mock\.js";' -Message 'G11 数据和显示角色必须来自共享夹具 owner'
|
||
Assert-Contains -Content $g11 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'G11 必须消费唯一放弃确认控制器'
|
||
Assert-Contains -Content $g11 -Expected 'import { goBack, handleBackPress, runBackGuard } from "@/utils/navigation.js";' -Message 'G11 必须使用统一导航守卫'
|
||
Assert-Contains -Content $g11 -Expected 'genealogyId.value = String(query.genealogyId || "");' -Message 'G11 家谱 ID 必须保持词法字符串'
|
||
Assert-Contains -Content $g11 -Expected 'getGenealogyFixtureAccess(genealogyId.value).accessRole !== "owner"' -Message 'G11 非 owner 必须失败关闭'
|
||
Assert-Contains -Content $g11 -Expected 'const isDirty = computed(' -Message 'G11 必须识别未保存表单'
|
||
Assert-Contains -Content $g11 -Expected '@back="requestBack"' -Message 'G11 页头必须复用返回守卫'
|
||
Assert-Contains -Content $g11 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G11 Android 返回必须使用同步适配器'
|
||
Assert-Contains -Content $g11 -Expected 'discardConfirmation.dispose();' -Message 'G11 卸载必须释放确认等待者'
|
||
Assert-Contains -Content $g11 -Expected '本地预览已更新,尚未提交服务器' -Message 'G11 本地保存必须明确未提交服务器'
|
||
Assert-Contains -Content $g11 -Expected 'genealogyDraft.name.trim().length > 20' -Message 'G11 名称运行时上限必须与输入控件 20 字约束一致'
|
||
Assert-Contains -Content $g11 -Expected '@click="handleResultAction"' -Message 'G11 错误与无权限状态必须通过受控动作处理'
|
||
foreach ($forbidden in @('settingsFixtures', '@click="settingsState = ''form''"', 'finishPage(', 'genealogy-settings-updated')) {
|
||
Assert-NotContains -Content $g11 -Unexpected $forbidden -Message "G11 不得绕过权限或伪造后端保存:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $g12 -Expected 'import AppDialog from "@/components/AppDialog.vue";' -Message 'G12 编辑取消必须使用项目对话框'
|
||
Assert-Matches -Content $g12 -Pattern '(?s)import \{\s*findGenealogyFixture,\s*getGenealogyFixtureAccess,\s*\} from "@/data/mock\.js";' -Message 'G12 数据与维护权限必须来自共享夹具解析器'
|
||
Assert-Contains -Content $g12 -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message 'G12 必须消费唯一放弃确认控制器'
|
||
Assert-Contains -Content $g12 -Expected 'import { handleBackPress, runBackGuard } from "@/utils/navigation.js";' -Message 'G12 必须使用统一返回守卫'
|
||
Assert-Contains -Content $g12 -Expected 'genealogyId.value = String(query.genealogyId || "");' -Message 'G12 家谱 ID 必须保持词法字符串'
|
||
Assert-Contains -Content $g12 -Expected 'const canManage = ref(false);' -Message 'G12 必须显式持有维护权限'
|
||
Assert-Contains -Content $g12 -Expected 'const editorOrigin = ref("list");' -Message 'G12 必须记录编辑器来自列表还是空状态'
|
||
Assert-Contains -Content $g12 -Expected 'const editorSnapshot = ref(null);' -Message 'G12 必须保存进入编辑器前的草稿快照'
|
||
Assert-Contains -Content $g12 -Expected 'const isDirty = computed(() =>' -Message 'G12 必须识别编辑器脏状态'
|
||
Assert-Contains -Content $g12 -Expected 'const requestLeaveEditor = async () =>' -Message 'G12 取消必须经过统一脏表单确认'
|
||
Assert-Contains -Content $g12 -Expected 'restoreEditorSnapshot();' -Message 'G12 放弃编辑必须恢复进入前快照'
|
||
Assert-Contains -Content $g12 -Expected '@back="requestBack"' -Message 'G12 页头必须复用返回守卫'
|
||
Assert-Contains -Content $g12 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'G12 Android 返回必须使用同步适配器'
|
||
Assert-Contains -Content $g12 -Expected 'discardConfirmation.dispose();' -Message 'G12 卸载必须释放确认等待者'
|
||
foreach ($forbidden in @('@click="poemState = ''list''"', 'finishPage(', 'generation-poems-updated')) {
|
||
Assert-NotContains -Content $g12 -Unexpected $forbidden -Message "G12 不得静默丢弃编辑或伪造后端保存:$forbidden"
|
||
}
|
||
|
||
foreach ($entry in @(
|
||
@{ Key = 'T01'; Content = $t01 },
|
||
@{ Key = 'T03'; Content = $t03 },
|
||
@{ Key = 'T04'; Content = $t04 },
|
||
@{ Key = 'T05'; Content = $t05 },
|
||
@{ Key = 'T06'; Content = $t06 },
|
||
@{ Key = 'T07'; Content = $t07 },
|
||
@{ Key = 'T08'; Content = $t08 }
|
||
)) {
|
||
foreach ($forbidden in @('uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch', '/pages/')) {
|
||
Assert-NotContains -Content $entry.Content -Unexpected $forbidden -Message "$($entry.Key) 必须只使用导航网关:$forbidden"
|
||
}
|
||
}
|
||
|
||
foreach ($mapping in @(
|
||
'openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")',
|
||
'openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id), mode: "rank" }, "T01")',
|
||
'openPage("T07", { genealogyId: genealogyId.value }, "T01")'
|
||
)) {
|
||
Assert-Contains -Content $t01 -Expected $mapping -Message "T01 缺少规范入口:$mapping"
|
||
}
|
||
foreach ($required in @('routeKey: "T04"', 'params.relationType = action.relationType;', 'return openPage(action.routeKey, params, "T01");')) {
|
||
Assert-Contains -Content $t01 -Expected $required -Message "T01 人物操作面板缺少 T04 关系入口:$required"
|
||
}
|
||
Assert-Contains -Content $t03 -Expected 'consumeNavigationResult("T03")' -Message 'T03 必须消费 single 页面激活请求'
|
||
Assert-Contains -Content $t03 -Expected 'internalTrail: trailIndex.value > 0' -Message 'T03 返回必须优先消费页内成员轨迹'
|
||
Assert-Contains -Content $t03 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'T03 Android 返回必须复用页内轨迹守卫'
|
||
Assert-Contains -Content $t07 -Expected 'openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")' -Message 'T07 必须进入唯一 T03 实例'
|
||
Assert-Contains -Content $t08 -Expected 'goRoot("G01")' -Message 'T08 权限失效动作必须进入 G01 根语义'
|
||
Assert-Contains -Content $t08 -Expected 'goBack()' -Message 'T08 普通动作必须按真实栈返回'
|
||
|
||
foreach ($form in @(
|
||
@{ Key = 'T04'; Content = $t04; Return = 'returnTo("T01", { genealogyId: genealogyId.value })'; Write = 'appApi.createRelatedPerson(' },
|
||
@{ Key = 'T05'; Content = $t05; Return = 'return goBack();'; Write = 'appApi.updatePerson(' }
|
||
)) {
|
||
Assert-Contains -Content $form.Content -Expected 'appApi.getPerson(' -Message "$($form.Key) 必须从真实后端读取当前成员"
|
||
Assert-Contains -Content $form.Content -Expected 'createRequestController' -Message "$($form.Key) 成员读取必须可取消"
|
||
Assert-Contains -Content $form.Content -Expected $form.Write -Message "$($form.Key) 必须使用已核对的远端写入 owner"
|
||
Assert-Contains -Content $form.Content -Expected $form.Return -Message "$($form.Key) 写入成功后必须保留规范返回目标"
|
||
foreach ($forbidden in @('finishPage(', 'relative-created', 'member-updated', 'relationship-updated', 'setTimeout(')) {
|
||
Assert-NotContains -Content $form.Content -Unexpected $forbidden -Message "$($form.Key) 不得在真实写入前伪造结果:$forbidden"
|
||
}
|
||
}
|
||
foreach ($required in @('appApi.getPerson(', 'createRequestController', '"unavailable"', 'goBack()')) {
|
||
Assert-Contains -Content $t06 -Expected $required -Message "T06 原子排行合同未收紧时必须显式关闭提交:$required"
|
||
}
|
||
Assert-Matches -Content $routes -Pattern '(?s)T01:\s*defineRoute\(\{(?:(?!resultOperations).)*?\}\),\s*T03:' -Message 'T01 当前不得预留 mutation 结果能力'
|
||
Assert-Matches -Content $routes -Pattern '(?s)T03:\s*defineRoute\(\{.*?resultOperations:\s*\["member-open-requested"\]' -Message 'T03 当前只能登记 single 激活结果'
|
||
|
||
foreach ($entry in @(
|
||
@{ Key = 'F01'; Content = $f01 },
|
||
@{ Key = 'F02'; Content = $f02 },
|
||
@{ Key = 'F03'; Content = $f03 },
|
||
@{ Key = 'F04'; Content = $f04 },
|
||
@{ Key = 'F05'; Content = $f05 },
|
||
@{ Key = 'F06'; Content = $f06 },
|
||
@{ Key = 'F07'; Content = $f07 },
|
||
@{ Key = 'F08'; Content = $f08 },
|
||
@{ Key = 'F09'; Content = $f09 },
|
||
@{ Key = 'F10'; Content = $f10 }
|
||
)) {
|
||
foreach ($forbidden in @('uni.navigateTo', 'uni.navigateBack', 'uni.redirectTo', 'uni.reLaunch', '/pages/', 'finishPage(')) {
|
||
Assert-NotContains -Content $entry.Content -Unexpected $forbidden -Message "$($entry.Key) 必须只使用无伪结果的导航网关:$forbidden"
|
||
}
|
||
}
|
||
|
||
Assert-Contains -Content $f01 -Expected 'goRoot("F01", { genealogyId: resolvedGenealogyId })' -Message 'F01 从全局解析家谱后必须先规范化根页 URL'
|
||
Assert-Contains -Content $f01 -Expected '动态列表待后端字段合同' -Message 'F01 必须明确动态列表读取仍被字段合同阻塞'
|
||
Assert-NotContains -Content $f01 -Unexpected 'listFamilyFeedFixtures' -Message 'F01 不得把本地动态夹具伪装成服务端列表'
|
||
foreach ($mapping in @(
|
||
'openPage("F02", { genealogyId: genealogyId.value }, "F01")',
|
||
'return openPage(routes[key], { genealogyId: genealogyId.value }, "F01");'
|
||
)) {
|
||
Assert-Contains -Content $f01 -Expected $mapping -Message "F01 缺少保留家谱身份的规范入口:$mapping"
|
||
}
|
||
|
||
foreach ($form in @(
|
||
@{ Key = 'F02'; Content = $f02 },
|
||
@{ Key = 'F06'; Content = $f06 },
|
||
@{ Key = 'F09'; Content = $f09 }
|
||
)) {
|
||
Assert-Contains -Content $form.Content -Expected 'import { createDiscardConfirmation } from "@/utils/discard-confirmation.js";' -Message "$($form.Key) 必须消费唯一放弃确认 owner"
|
||
Assert-Contains -Content $form.Content -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message "$($form.Key) Android 返回必须复用页面守卫"
|
||
Assert-Contains -Content $form.Content -Expected 'runBackGuard({' -Message "$($form.Key) 页头与 Android 返回必须共享守卫上下文"
|
||
Assert-Contains -Content $form.Content -Expected 'submitting:' -Message "$($form.Key) 校验中必须阻止离开"
|
||
Assert-Contains -Content $form.Content -Expected 'discardConfirmation.dispose();' -Message "$($form.Key) 卸载必须释放确认等待者"
|
||
}
|
||
|
||
Assert-Contains -Content $f02 -Expected '尚未提交服务器' -Message 'F02 只能生成明确未提交的本地预览'
|
||
Assert-Contains -Content $f02 -Expected 'returnTo("F01", { genealogyId: genealogyId.value })' -Message 'F02 必须无结果返回同一家谱 F01'
|
||
Assert-Contains -Content $f03 -Expected 'feed-detail-contract-note' -Message 'F03 必须明确详情与评论读取仍被合同阻塞'
|
||
Assert-Contains -Content $f03 -Expected 'returnTo("F01", { genealogyId: genealogyId.value })' -Message 'F03 必须安全返回同一家谱 F01'
|
||
foreach ($forbidden in @('appApi.', 'findFamilyFeedFixture', 'feedComments', 'commentDraft', 'submitComment')) {
|
||
Assert-NotContains -Content $f03 -Unexpected $forbidden -Message "F03 后端门禁未关闭前不得保留宽接口或本地评论路径:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $f01 -Expected 'feedState.value = hasValidContext.value ? "unavailable" : "error";' -Message 'F01 必须在解析家谱上下文后进入明确的不可用或错误状态'
|
||
|
||
Assert-Contains -Content $f04 -Expected 'listFamilyArticleFixtures(genealogyId.value)' -Message 'F04 必须读取同一家谱的共享谱文 owner'
|
||
Assert-Contains -Content $f04 -Expected ':action="hasValidContext ? ''新建'' : ''''"' -Message 'F04 无效家谱状态不得暴露新建入口'
|
||
if ($f04 -notmatch '(?s)const createArticle = \(\) =>\s*hasValidContext\.value\s*\?\s*openPage\(') {
|
||
throw 'F04 新建处理器必须在调用导航网关前再次校验有效家谱上下文'
|
||
}
|
||
Assert-Contains -Content $f05 -Expected 'findFamilyArticleFixture(genealogyId.value, articleId.value)' -Message 'F05 必须按复合身份精确查询谱文'
|
||
Assert-Contains -Content $f05 -Expected 'disabled label="收藏暂未开放"' -Message 'F05 无线上端点时必须禁用收藏二态'
|
||
Assert-NotContains -Content $f05 -Unexpected 'toggleFavorite' -Message 'F05 不得保留本地收藏伪持久化'
|
||
Assert-Contains -Content $f06 -Expected '(editorMode.value === "create" && !articleId.value)' -Message 'F06 create 必须禁止 articleId'
|
||
Assert-Contains -Content $f06 -Expected '(editorMode.value === "edit" && Boolean(article))' -Message 'F06 edit 必须要求精确文章'
|
||
Assert-Contains -Content $f06 -Expected '当前尚未提交服务器' -Message 'F06 必须明确本地预览没有保存文章'
|
||
|
||
Assert-Contains -Content $f07 -Expected 'listFamilyAlbumFixtures(genealogyId.value)' -Message 'F07 必须读取同一家谱的共享相册 owner'
|
||
Assert-Contains -Content $f07 -Expected 'localAlbumPreview.value = Object.freeze({ name });' -Message 'F07 草稿只能进入独立本地预览'
|
||
Assert-NotContains -Content $f07 -Unexpected 'albums.value.unshift' -Message 'F07 不得把未提交草稿插入正式相册列表'
|
||
Assert-Contains -Content $f07 -Expected 'onBackPress((event) => handleBackPress(event, requestBack));' -Message 'F07 弹层与草稿必须接管 Android 返回'
|
||
Assert-Contains -Content $f08 -Expected 'findFamilyAlbumFixture(genealogyId.value, albumId.value)' -Message 'F08 必须按复合身份精确查询相册'
|
||
Assert-Contains -Content $f08 -Expected 'transientOpen: previewVisible.value' -Message 'F08 返回必须优先关闭照片预览'
|
||
Assert-Contains -Content $f08 -Expected 'openPage(' -Message 'F08 添加照片必须走导航网关'
|
||
Assert-Contains -Content $f09 -Expected 'findFamilyAlbumFixture(genealogyId.value, albumId.value)' -Message 'F09 必须按复合身份锁定上传目标'
|
||
Assert-Contains -Content $f09 -Expected '照片尚未上传' -Message 'F09 本地预览必须明确未上传'
|
||
foreach ($forbidden in @('uploading', 'uploaded', 'success', 'retryFailed', '/genealogy/app/files/upload', 'media-uploaded')) {
|
||
Assert-NotContains -Content $f09 -Unexpected $forbidden -Message "F09 不得保留假上传状态或陈旧接口:$forbidden"
|
||
}
|
||
Assert-Contains -Content $f10 -Expected 'returnTo("F01", { genealogyId: genealogyId.value })' -Message 'F10 必须保留家谱身份返回 F01'
|
||
|
||
foreach ($operation in @('feed-created', 'article-created', 'article-updated', 'media-uploaded')) {
|
||
Assert-NotContains -Content $routes -Unexpected $operation -Message "F 系列真实写入前不得预注册结果:$operation"
|
||
}
|
||
|
||
foreach ($page in @($r01,$r02,$r03,$r04,$r05,$r06,$r07,$r08,$r09,$r10,$r11)) {
|
||
foreach ($forbidden in @('uni.navigateTo(', 'uni.navigateBack(', 'uni.redirectTo(', 'uni.reLaunch(', '/pages/records/')) {
|
||
Assert-NotContains -Content $page -Unexpected $forbidden -Message "R 系列必须只通过导航网关消费活动路由:$forbidden"
|
||
}
|
||
}
|
||
foreach ($contract in @(
|
||
@{ Content = $r01; RouteKey = 'R02' },
|
||
@{ Content = $r02; RouteKey = 'R08' },
|
||
@{ Content = $r02; RouteKey = 'R09' },
|
||
@{ Content = $r03; RouteKey = 'R04' },
|
||
@{ Content = $r05; RouteKey = 'R06' },
|
||
@{ Content = $r05; RouteKey = 'R07' },
|
||
@{ Content = $r06; RouteKey = 'R07' },
|
||
@{ Content = $t03; RouteKey = 'R08' },
|
||
@{ Content = $t03; RouteKey = 'R09' }
|
||
)) {
|
||
Assert-Matches -Content $contract.Content -Pattern "openPage\(\s*`"$($contract.RouteKey)`"" -Message "R 系列缺少注册表流程:$($contract.RouteKey)"
|
||
}
|
||
foreach ($operation in @('person-created','person-updated','person-deleted','gift-created','gift-updated','gift-deleted','ritual-created','ritual-updated','ritual-deleted')) {
|
||
Assert-NotContains -Content $routes -Unexpected $operation -Message "R 系列真实写入前不得预注册结果:$operation"
|
||
}
|
||
|
||
foreach ($page in @(
|
||
@{ Key = 'N01'; Content = $n01 },
|
||
@{ Key = 'N02'; Content = $n02 },
|
||
@{ Key = 'M01'; Content = $m01 },
|
||
@{ Key = 'M02'; Content = $m02 },
|
||
@{ Key = 'M03'; Content = $m03 },
|
||
@{ Key = 'M04'; Content = $m04 },
|
||
@{ Key = 'M05'; Content = $m05 },
|
||
@{ Key = 'M06'; Content = $m06 },
|
||
@{ Key = 'M07'; Content = $m07 },
|
||
@{ Key = 'M08'; Content = $m08 },
|
||
@{ Key = 'M09'; Content = $m09 },
|
||
@{ Key = 'M10'; Content = $m10 }
|
||
)) {
|
||
foreach ($forbidden in @(
|
||
'uni.navigateTo',
|
||
'uni.navigateBack',
|
||
'uni.redirectTo',
|
||
'uni.reLaunch',
|
||
'getCurrentPages(',
|
||
'<navigator',
|
||
'/pages/',
|
||
'finishPage('
|
||
)) {
|
||
Assert-NotContains -Content $page.Content -Unexpected $forbidden -Message "$($page.Key) 必须只消费唯一导航网关且不得伪造流程结果:$forbidden"
|
||
}
|
||
}
|
||
|
||
foreach ($contract in @(
|
||
@{ Key = 'N01'; ParamToken = 'optionalParams: ["genealogyId"]'; SourceToken = 'allowedSources: ["G01", "M01"]' },
|
||
@{ Key = 'N02'; ParamToken = 'requiredParams: ["id"]'; SourceToken = 'allowedSources: ["N01"]' },
|
||
@{ Key = 'M02'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M01"]' },
|
||
@{ Key = 'M03'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||
@{ Key = 'M04'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M03"]' },
|
||
@{ Key = 'M05'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M03"]' },
|
||
@{ Key = 'M06'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||
@{ Key = 'M07'; ParamToken = 'kind: "flow"'; SourceToken = 'allowedSources: ["M01", "M06"]' },
|
||
@{ Key = 'M08'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||
@{ Key = 'M09'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' },
|
||
@{ Key = 'M10'; ParamToken = 'kind: "page"'; SourceToken = 'allowedSources: ["M01"]' }
|
||
)) {
|
||
$routePattern = '(?s)' +
|
||
[regex]::Escape("$($contract.Key): defineRoute({") +
|
||
'(?:(?!\}\),).)*' +
|
||
[regex]::Escape($contract.ParamToken) +
|
||
'(?:(?!\}\),).)*' +
|
||
[regex]::Escape($contract.SourceToken)
|
||
Assert-Matches -Content $routes -Pattern $routePattern -Message "$($contract.Key) 路由参数、页面语义或唯一来源漂移"
|
||
}
|
||
Assert-Matches -Content $routes -Pattern '(?s)M01:\s*defineRoute\(\{\s*path:\s*"/pages/profile/m01-profile-home",\s*kind:\s*"root",\s*parent:\s*null,?\s*\}\)' -Message 'M01 必须保留唯一 profile 根语义'
|
||
|
||
Assert-Matches -Content $routes -Pattern '(?s)const defineNoticeTarget = \(routeKey, params\) =>\s*Object\.freeze\(\{ routeKey, params: Object\.freeze\(params\) \}\);' -Message '通知目标定义及参数列表必须不可变'
|
||
Assert-Matches -Content $routes -Pattern '(?s)export const NOTICE_TARGETS = Object\.freeze\(\{\s*GENEALOGY_REVIEW: defineNoticeTarget\("G10", \["genealogyId"\]\),\s*GENEALOGY_HOME: defineNoticeTarget\("G01", \["genealogyId"\]\),\s*\}\);' -Message '通知目标白名单只能把审核映射到 G10、家谱首页映射到 G01'
|
||
foreach ($token in @(
|
||
'export const openNoticeTarget = (targetType, params, sourceKey = "N02") =>',
|
||
'assertScalarString("通知目标类型", targetType);',
|
||
'assertScalarString("通知来源", sourceKey);',
|
||
'if (!hasOwn(NOTICE_TARGETS, targetType))',
|
||
'if (!["N01", "N02"].includes(sourceKey))',
|
||
'const normalizedParams = snapshotDataRecord("通知目标参数", params);',
|
||
'const expectedParams = new Set(target.params);',
|
||
'if (!expectedParams.has(name))',
|
||
'assertScalarString(name, normalizedParams[name]);',
|
||
'if (!hasOwn(normalizedParams, name))',
|
||
'? goRoot(target.routeKey, normalizedParams)',
|
||
': openPage(target.routeKey, normalizedParams, sourceKey);'
|
||
)) {
|
||
Assert-Contains -Content $navigation -Expected $token -Message "通知导航网关缺少安全边界:$token"
|
||
}
|
||
$noticeWhitelistIndex = $navigation.IndexOf('if (!hasOwn(NOTICE_TARGETS, targetType))')
|
||
$noticeLookupIndex = $navigation.IndexOf('const target = NOTICE_TARGETS[targetType];')
|
||
if ($noticeWhitelistIndex -lt 0 -or $noticeLookupIndex -le $noticeWhitelistIndex) {
|
||
throw '通知导航网关必须先验证白名单,再读取目标定义'
|
||
}
|
||
|
||
foreach ($token in @(
|
||
"targetType: 'GENEALOGY_REVIEW'",
|
||
"targetType: 'GENEALOGY_HOME'",
|
||
"targetParams: { genealogyId: '1001' }",
|
||
'export const listNotificationFixtures',
|
||
'export const findNotificationFixture'
|
||
)) {
|
||
Assert-Contains -Content $mock -Expected $token -Message "通知夹具 owner 缺少安全目标字段:$token"
|
||
}
|
||
foreach ($forbidden in @('detailId', 'targetUrl', 'returnUrl', "target: '/pages/", 'const noticeDetails')) {
|
||
Assert-NotContains -Content $mock -Unexpected $forbidden -Message "通知夹具不得保留重复身份、页面 owner 或可执行路径:$forbidden"
|
||
Assert-NotContains -Content $n01 -Unexpected $forbidden -Message "N01 不得保留重复身份、页面 owner 或可执行路径:$forbidden"
|
||
Assert-NotContains -Content $n02 -Unexpected $forbidden -Message "N02 不得保留重复身份、页面 owner 或可执行路径:$forbidden"
|
||
}
|
||
|
||
Assert-Contains -Content $n01 -Expected 'import { listNotificationFixtures } from "@/data/mock.js";' -Message 'N01 必须读取共享通知夹具 owner'
|
||
Assert-Matches -Content $n01 -Pattern '(?s)openPage\(\s*"N02",\s*\{ id: String\(item\.id\) \},\s*"N01",?\s*\)' -Message 'N01 必须只携带词法消息 ID 并声明 N01 来源进入 N02'
|
||
Assert-Matches -Content $n01 -Pattern '(?s)genealogyId\.value\s*\?\s*openPage\(\s*"G10",\s*\{ genealogyId: genealogyId\.value \},\s*"N01",?\s*\)' -Message 'N01 审核入口必须缺少家谱 ID 时失败关闭,并通过 G10 注册路由'
|
||
Assert-Contains -Content $n02 -Expected 'findNotificationFixture(noticeId.value)' -Message 'N02 必须按共享 owner 的词法消息 ID 精确查询'
|
||
Assert-Contains -Content $n02 -Expected 'noticeDetail.value.unread = false;' -Message 'N02 已读动作必须更新共享 owner 的 unread 字段'
|
||
Assert-NotContains -Content $n02 -Unexpected 'noticeDetail.read' -Message 'N02 不得创建与 unread 冲突的 read 字段'
|
||
Assert-Contains -Content $n02 -Expected 'getGenealogyFixtureAccess' -Message 'N02 打开业务目标前必须校验家谱访问权限'
|
||
Assert-Matches -Content $n02 -Pattern '(?s)return await navigateNoticeTarget\(\s*noticeDetail\.value\.targetType,\s*noticeDetail\.value\.targetParams,\s*\);' -Message 'N02 只能把声明式目标类型和参数交给安全通知网关'
|
||
if ($n02 -match 'noticeDetail\.value\.target(?![A-Za-z0-9_$])') {
|
||
throw 'N02 不得消费可执行 target 字段'
|
||
}
|
||
Assert-Contains -Content $n02 -Expected 'const backToMessages = () => returnTo("N01", {});' -Message 'N02 页头必须通过返回网关回到 N01'
|
||
Assert-Contains -Content $n02 -Expected 'onBackPress((event) => handleBackPress(event, backToMessages));' -Message 'N02 Android 返回必须复用唯一 N01 返回动作'
|
||
|
||
foreach ($mapping in @(
|
||
'openPage("M02", {}, "M01")',
|
||
'openPage("N01", {}, "M01")',
|
||
'routeKey: "M03"',
|
||
'routeKey: "M06"',
|
||
'routeKey: "M08"',
|
||
'routeKey: "M09"',
|
||
'routeKey: "M10"',
|
||
'openPage(item.routeKey, {}, "M01")'
|
||
)) {
|
||
Assert-Contains -Content $m01 -Expected $mapping -Message "M01 缺少注册表入口:$mapping"
|
||
}
|
||
foreach ($mapping in @('routeKey: "M04"', 'routeKey: "M05"', 'openPage(item.routeKey, {}, "M03")')) {
|
||
Assert-Contains -Content $m03 -Expected $mapping -Message "M03 缺少注册表入口:$mapping"
|
||
}
|
||
Assert-Contains -Content $m06 -Expected 'openPage("M07", {}, "M06")' -Message 'M06 联系支持必须进入注册的 M07 流程'
|
||
|
||
foreach ($form in @(
|
||
@{ Key = 'M02'; Content = $m02 },
|
||
@{ Key = 'M04'; Content = $m04 },
|
||
@{ Key = 'M05'; Content = $m05 },
|
||
@{ Key = 'M07'; Content = $m07 }
|
||
)) {
|
||
foreach ($token 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"'
|
||
)) {
|
||
Assert-Contains -Content $form.Content -Expected $token -Message "$($form.Key) 缺少脏表单返回守卫:$token"
|
||
}
|
||
}
|
||
|
||
foreach ($page in @(
|
||
@{ Key = 'M08'; Content = $m08 },
|
||
@{ Key = 'M09'; Content = $m09 },
|
||
@{ Key = 'M10'; Content = $m10 }
|
||
)) {
|
||
foreach ($token in @(
|
||
'custom-back',
|
||
'@back="requestBack"',
|
||
'runBackGuard({',
|
||
'transientOpen:',
|
||
'onBackPress((event) => handleBackPress(event, requestBack));',
|
||
':close-on-mask="false"'
|
||
)) {
|
||
Assert-Contains -Content $page.Content -Expected $token -Message "$($page.Key) 缺少浮层优先返回守卫:$token"
|
||
}
|
||
}
|
||
Assert-Matches -Content $m10 -Pattern '(?s)const confirmLogout = async \(\) => \{.*?await appApi\.logout\(\{ requestController: logoutRequestController \}\);.*?finally \{\s*session\.clear\(\);\s*logoutVisible\.value = false;.*?\}\s*return goRoot\("A01"\);\s*\};' -Message 'M10 退出必须先尝试唯一远端 owner,再在所有结果下清理本机会话并进入 A01 根语义'
|
||
|
||
Write-Output 'NAVIGATION-FLOW-CONTRACT PASS AUTH G01-G12 T01-T08 F01-F10 R01-R11 N01-N02 M01-M10'
|