完成50%
This commit is contained in:
@@ -1,13 +1,26 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Contains {
|
||||
param([string]$Content, [string]$Expected, [string]$Message)
|
||||
if ($Content -notmatch [regex]::Escape($Expected)) { throw $Message }
|
||||
if (-not $Content.Contains($Expected)) { throw $Message }
|
||||
}
|
||||
|
||||
function Assert-NotContains {
|
||||
param([string]$Content, [string]$Unexpected, [string]$Message)
|
||||
if ($Content.Contains($Unexpected)) { throw $Message }
|
||||
}
|
||||
|
||||
function Assert-Matches {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$profiles = Get-Content -LiteralPath (Join-Path $root 'styles/adaptive-frame-profiles.scss') -Raw -Encoding utf8
|
||||
$mock = Get-Content -LiteralPath (Join-Path $root 'data/mock.js') -Raw -Encoding utf8
|
||||
$api = Get-Content -LiteralPath (Join-Path $root 'utils/api.js') -Raw -Encoding utf8
|
||||
$paths = @(
|
||||
'pages/tree/t01-tree-overview.vue',
|
||||
'pages/tree/t03-member-profile.vue',
|
||||
'pages/tree/t04-add-relative.vue',
|
||||
'pages/tree/t05-edit-member.vue',
|
||||
@@ -19,23 +32,77 @@ $pages = @{}
|
||||
foreach ($path in $paths) {
|
||||
$content = Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding utf8
|
||||
if ($content -match '<ModulePage(?:\s|>)') { throw "$path must not retain ModulePage" }
|
||||
if ($content -match "@/utils/api\.js|\bappApi\b") { throw "$path must not connect the API layer" }
|
||||
if ($content -match "@/utils/api\.js|\bappApi\b") { throw "$path must not connect the API layer in navigation task 6" }
|
||||
if ($content -match '\buni\.(?:navigateTo|navigateBack|redirectTo|reLaunch|switchTab)\b') {
|
||||
throw "$path must use the navigation gateway exclusively"
|
||||
}
|
||||
if ($content.Contains('/pages/')) { throw "$path must not own route literals" }
|
||||
$pages[$path] = $content
|
||||
}
|
||||
|
||||
foreach ($path in @('pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-directory.vue', 'pages/tree/t08-member-states.vue')) {
|
||||
foreach ($path in @('pages/tree/t01-tree-overview.vue', 'pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-directory.vue', 'pages/tree/t08-member-states.vue')) {
|
||||
Assert-Contains $pages[$path] 'ModulePageBackground' "$path must use the tree module background"
|
||||
}
|
||||
|
||||
$t03 = $pages['pages/tree/t03-member-profile.vue']
|
||||
foreach ($required in @('member-state--detail', 'member-state--error', 'adaptive.adaptive-tree-panel', 'adaptive.adaptive-tree-field', '/pages/tree/t05-edit-member', 'query.personId')) {
|
||||
Assert-Contains $t03 $required "Missing T03 contract: $required"
|
||||
$t01 = $pages['pages/tree/t01-tree-overview.vue']
|
||||
Assert-NotContains $t01 'consumeNavigationResult(' 'T01 must not pre-register a mutation consumer before real writes exist'
|
||||
foreach ($mapping in @(
|
||||
'openPage("T03", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")',
|
||||
'openPage("T07", { genealogyId: genealogyId.value }, "T01")',
|
||||
'openPage("T06", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")',
|
||||
'openPage("T04", { genealogyId: genealogyId.value, personId: String(selected.value.id) }, "T01")'
|
||||
)) {
|
||||
Assert-Contains $t01 $mapping "T01 missing gateway target: $mapping"
|
||||
}
|
||||
Assert-Matches $t01 '(?s)openPage\(\s*"T04",\s*\{ genealogyId: genealogyId\.value, mode: "first" \},\s*"T01",?\s*\)' 'T01 empty state must open first-member mode through T04 route contract'
|
||||
|
||||
$t03 = $pages['pages/tree/t03-member-profile.vue']
|
||||
foreach ($required in @(
|
||||
'member-state--detail',
|
||||
'member-state--restricted',
|
||||
'member-state--error',
|
||||
'adaptive.adaptive-tree-panel',
|
||||
'adaptive.adaptive-tree-field',
|
||||
'<PageHeader title=',
|
||||
'custom-back',
|
||||
'@back="requestBack"',
|
||||
'const memberTrail = reactive([]);',
|
||||
'const trailIndex = ref(-1);',
|
||||
'const loadMember = async (nextPersonId) =>',
|
||||
'const initializeMemberTrail = async (initialPersonId) =>',
|
||||
'memberTrail.splice(0, memberTrail.length, normalizedPersonId);',
|
||||
'trailIndex.value = 0;',
|
||||
'const openRelative = async (nextPersonId) =>',
|
||||
'memberTrail.splice(trailIndex.value + 1);',
|
||||
'memberTrail.push(normalizedPersonId);',
|
||||
'trailIndex.value = memberTrail.length - 1;',
|
||||
'const popMemberTrail = async () =>',
|
||||
'memberTrail.splice(targetIndex, 1);',
|
||||
'internalTrail: trailIndex.value > 0',
|
||||
'"pop-internal-trail": popMemberTrail',
|
||||
'consumeNavigationResult("T03")',
|
||||
'result?.operation === "member-open-requested"',
|
||||
'onBackPress((event) => handleBackPress(event, requestBack));',
|
||||
'openPage("T05", { genealogyId: genealogyId.value, personId: personId.value }, "T03")',
|
||||
'openPage("T08", { genealogyId: genealogyId.value, personId: personId.value }, "T03")'
|
||||
)) {
|
||||
Assert-Contains $t03 $required "Missing T03 single-instance trail contract: $required"
|
||||
}
|
||||
Assert-Matches $t03 '(?s)const openRelative = async \(nextPersonId\) => \{.*?const loaded = await loadMember\(normalizedPersonId\);\s*if \(!loaded\) return false;.*?memberTrail\.push\(normalizedPersonId\)' 'T03 must append a relative only after a successful read'
|
||||
Assert-Matches $t03 '(?s)const initializeMemberTrail = async \(initialPersonId\) => \{\s*memberTrail\.splice\(0, memberTrail\.length\);\s*trailIndex\.value = -1;.*?if \(!loaded\) return false;.*?memberTrail\.splice\(0, memberTrail\.length, normalizedPersonId\)' 'T03 initial failure must leave the trail empty'
|
||||
Assert-Contains $t03 ':data-current-person-id="personId"' 'T03 runtime smoke needs a semantic current-person marker'
|
||||
Assert-Contains $t03 ':data-trail-length="memberTrail.length"' 'T03 runtime smoke must observe trail initialization without internal access'
|
||||
Assert-Contains $t03 ':data-trail-index="trailIndex"' 'T03 runtime smoke must observe the current trail position'
|
||||
Assert-Contains $t03 ':data-person-id="relative.id"' 'T03 relatives need stable lexical identity in the rendered list'
|
||||
Assert-NotContains $t03 'genealogyContext' 'T03 must not fall back to mutable global genealogy context'
|
||||
Assert-Matches $t03 '(?s)const restricted\s*=\s*\["privacy",\s*"forbidden"\]\.includes\(fixture\.status\).*?memberState\.value\s*=\s*restricted\s*\?\s*"restricted"\s*:\s*"detail"' 'T03 must derive restricted presentation from the scoped member status'
|
||||
Assert-Contains $t03 'const canPreviewEdit = computed(() => memberState.value === "detail");' 'T03 may expose only a clearly local preview entry for unrestricted detail state'
|
||||
Assert-NotContains $t03 'canEdit' 'T03 must not treat a fixture field as a backend edit capability'
|
||||
|
||||
$specializedForms = @{
|
||||
't04-add-relative.vue' = @('add-state--form', 'add-state--success', 'add-relative-form', 'relationOptions')
|
||||
't05-edit-member.vue' = @('edit-state--form', 'edit-state--success', 'edit-member-form', 'memberFixtures')
|
||||
't06-edit-relationship.vue' = @('relationship-state--form', 'relationship-state--conflict', 'relationship-form', 'relationshipOptions')
|
||||
't04-add-relative.vue' = @('add-state--form', 'add-state--preview', 'add-relative-form', 'relationOptions')
|
||||
't05-edit-member.vue' = @('edit-state--form', 'edit-state--preview', 'edit-member-form', 'findTreeMemberPresentationFixture')
|
||||
't06-edit-relationship.vue' = @('relationship-state--form', 'relationship-state--preview', 'relationship-form', 'relationshipOptions')
|
||||
}
|
||||
foreach ($entry in $specializedForms.GetEnumerator()) {
|
||||
$content = $pages["pages/tree/$($entry.Key)"]
|
||||
@@ -43,27 +110,176 @@ foreach ($entry in $specializedForms.GetEnumerator()) {
|
||||
throw "$($entry.Key) must own its business workflow instead of TreeMemberForm"
|
||||
}
|
||||
foreach ($required in $entry.Value) {
|
||||
Assert-Contains $content $required "Missing specialized form contract in $($entry.Key): $required"
|
||||
Assert-Contains -Content $content -Expected $required -Message ("Missing specialized form contract in {0}: {1}" -f $entry.Key, $required)
|
||||
}
|
||||
foreach ($required in @(
|
||||
'createDiscardConfirmation',
|
||||
'handleBackPress',
|
||||
'runBackGuard',
|
||||
'submitting: isSubmitting.value',
|
||||
'"block-submitting"',
|
||||
'onBackPress((event) => handleBackPress(event, requestBack));',
|
||||
'discardConfirmation.dispose();'
|
||||
)) {
|
||||
Assert-Contains -Content $content -Expected $required -Message ("{0} missing shared guarded local-preview contract: {1}" -f $entry.Key, $required)
|
||||
}
|
||||
Assert-NotContains -Content $content -Unexpected 'finishPage(' -Message ("{0} must not emit a server-success result before a real write succeeds" -f $entry.Key)
|
||||
foreach ($operation in @('relative-created', 'member-updated', 'relationship-updated')) {
|
||||
Assert-NotContains -Content $content -Unexpected $operation -Message ("{0} must not retain a pre-API mutation operation" -f $entry.Key)
|
||||
}
|
||||
Assert-Matches -Content $content -Pattern '\u5c1a\u672a\u63d0\u4ea4\u670d\u52a1\u5668' -Message ("{0} must disclose that its preview is not submitted" -f $entry.Key)
|
||||
}
|
||||
Assert-Contains -Content ($pages['pages/tree/t04-add-relative.vue']) -Expected 'returnTo("T01", { genealogyId: genealogyId.value })' -Message 'T04 local preview must return to T01 without a mutation result'
|
||||
Assert-Contains -Content ($pages['pages/tree/t05-edit-member.vue']) -Expected 'return goBack();' -Message 'T05 must preserve the existing single T03 route identity when leaving a relative preview'
|
||||
Assert-NotContains -Content ($pages['pages/tree/t05-edit-member.vue']) -Unexpected 'returnTo("T03"' -Message 'T05 must not replace the host T03 initial personId with its current in-page member'
|
||||
Assert-Contains -Content ($pages['pages/tree/t06-edit-relationship.vue']) -Expected 'returnTo("T01", { genealogyId: genealogyId.value })' -Message 'T06 local preview must return to T01 without a mutation result'
|
||||
|
||||
foreach ($submission in @(
|
||||
@{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Function = 'submitAdd'; State = 'addState.value' },
|
||||
@{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Function = 'saveMember'; State = 'editState.value' },
|
||||
@{ Key = 'T06'; Content = $pages['pages/tree/t06-edit-relationship.vue']; Function = 'saveRelationship'; State = 'relationshipState.value' }
|
||||
)) {
|
||||
$body = [regex]::Match(
|
||||
$submission.Content,
|
||||
"(?s)const $($submission.Function) = \(\) => \{.*?`n\};"
|
||||
).Value
|
||||
if (-not $body) { throw "$($submission.Key) submission function is not statically auditable" }
|
||||
if (-not $body.Contains('"preview"')) { throw "$($submission.Key) valid submission must stop at local preview" }
|
||||
if ($body -match '\b(?:returnTo|goBack|finishPage)\s*\(') {
|
||||
throw "$($submission.Key) local validation must not navigate automatically"
|
||||
}
|
||||
}
|
||||
if (([regex]::Matches($pages['pages/tree/t05-edit-member.vue'], 'baseline\.value = formSnapshot\.value;')).Count -ne 1) {
|
||||
throw 'T05 local preview must not clear the dirty baseline before a server write exists'
|
||||
}
|
||||
|
||||
$t07 = $pages['pages/tree/t07-member-directory.vue']
|
||||
foreach ($required in @('directory-state--list', 'directory-state--empty', 'directory-state--error', 'adaptive.adaptive-genealogy-list-card', 'directory-card__status', '/pages/tree/t03-member-profile')) {
|
||||
foreach ($required in @(
|
||||
'directory-state--list',
|
||||
'directory-state--empty',
|
||||
'directory-state--error',
|
||||
'adaptive.adaptive-genealogy-list-card',
|
||||
'directory-card__status',
|
||||
'listTreeMemberPresentationFixtures',
|
||||
'const isRestrictedMember =',
|
||||
'memberMeta(item)',
|
||||
'memberStatus(item)',
|
||||
'openPage("T03", { genealogyId: genealogyId.value, personId: String(item.id) }, "T07")'
|
||||
)) {
|
||||
Assert-Contains $t07 $required "Missing T07 contract: $required"
|
||||
}
|
||||
foreach ($privateTemplateRead in @('{{ item.generationName }}', '{{ item.branch }}', '{{ item.note }}')) {
|
||||
Assert-NotContains $t07 $privateTemplateRead "T07 must not render a restricted member private field directly: $privateTemplateRead"
|
||||
}
|
||||
foreach ($required in @(
|
||||
'const hasValidContext = computed(() => Boolean(genealogyId.value));',
|
||||
'!hasValidContext.value ? "error"',
|
||||
'v-if="hasValidContext"'
|
||||
)) {
|
||||
Assert-Contains $t07 $required "T07 must fail closed without genealogy context: $required"
|
||||
}
|
||||
|
||||
$t08 = $pages['pages/tree/t08-member-states.vue']
|
||||
foreach ($required in @('member-status--privacy', 'member-status--deceased', 'member-status--forbidden', 'adaptive.adaptive-tree-panel', 'query.state')) {
|
||||
foreach ($required in @(
|
||||
'member-status--privacy',
|
||||
'member-status--deceased',
|
||||
'member-status--forbidden',
|
||||
'adaptive.adaptive-tree-panel',
|
||||
'memberIdentityCopy',
|
||||
'姓名、世代与家族关系可见',
|
||||
'goRoot("G01")',
|
||||
'goBack()'
|
||||
)) {
|
||||
Assert-Contains $t08 $required "Missing T08 contract: $required"
|
||||
}
|
||||
Assert-NotContains $t08 '{{ member.branch }}' 'T08 must not assume a restricted member branch is visible'
|
||||
Assert-NotContains $t08 'genealogyContext' 'T08 must not fall back to mutable global genealogy context'
|
||||
foreach ($required in @(
|
||||
'const hasValidContext = computed(() => Boolean(genealogyId.value && personId.value));',
|
||||
'member.value = hasValidContext.value'
|
||||
)) {
|
||||
Assert-Contains $t08 $required "T08 must fail closed without its complete route identity: $required"
|
||||
}
|
||||
|
||||
$t06 = $pages['pages/tree/t06-edit-relationship.vue']
|
||||
foreach ($required in @(
|
||||
'const hasValidContext = computed(',
|
||||
'Boolean(genealogyId.value && memberById.value.has(personId.value))',
|
||||
'relationshipForm.sourceId = hasValidContext.value ? personId.value : "";',
|
||||
'relationshipState.value = !hasValidContext.value || query.state === "error"'
|
||||
)) {
|
||||
Assert-Contains $t06 $required "T06 must fail closed without a valid routed member: $required"
|
||||
}
|
||||
Assert-NotContains $t06 ': memberOptions[0].id' 'T06 must never replace a missing routed person with the first fixture member'
|
||||
|
||||
$treeOverview = $pages['pages/tree/t01-tree-overview.vue']
|
||||
foreach ($page in @(
|
||||
@{ Key = 'T01'; Content = $treeOverview },
|
||||
@{ Key = 'T03'; Content = $t03 },
|
||||
@{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue'] },
|
||||
@{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue'] },
|
||||
@{ Key = 'T06'; Content = $t06 },
|
||||
@{ Key = 'T07'; Content = $t07 },
|
||||
@{ Key = 'T08'; Content = $t08 }
|
||||
)) {
|
||||
Assert-Contains $page.Content '@/data/mock.js' "$($page.Key) must consume the shared tree-member fixture owner"
|
||||
if ($page.Content -match 'import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*["'']@/data/mock\.js["'']') {
|
||||
throw "$($page.Key) must consume snapshots instead of the mutable treeMembers store"
|
||||
}
|
||||
}
|
||||
foreach ($localOwner in @(
|
||||
@{ Key = 'T01'; Content = $treeOverview; Pattern = 'const\s+members\s*=\s*ref\(\[\s*\{' },
|
||||
@{ Key = 'T03'; Content = $t03; Pattern = 'const\s+memberFixtures\s*=' },
|
||||
@{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Pattern = 'const\s+memberFixtures\s*=' },
|
||||
@{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Pattern = 'const\s+memberFixtures\s*=' },
|
||||
@{ Key = 'T06'; Content = $t06; Pattern = 'const\s+memberOptions\s*=\s*\[' },
|
||||
@{ Key = 'T07'; Content = $t07; Pattern = 'const\s+members\s*=\s*\[' },
|
||||
@{ Key = 'T08'; Content = $t08; Pattern = 'const\s+memberFixtures\s*=' }
|
||||
)) {
|
||||
if ($localOwner.Content -match $localOwner.Pattern) {
|
||||
throw "$($localOwner.Key) retains a competing local tree-member fixture owner"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ownerContract in @(
|
||||
'export const treeMembers = [',
|
||||
'export const listTreeMemberFixtures = (genealogyId) =>',
|
||||
'export const findTreeMemberFixture = (genealogyId, personId) =>',
|
||||
'relatives: member.relatives.map((relative) => ({ ...relative }))'
|
||||
)) {
|
||||
Assert-Contains $mock $ownerContract "Shared tree-member owner missing: $ownerContract"
|
||||
}
|
||||
Assert-Matches $api '(?s)import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*''@/data/mock\.js''' 'utils/api.js must remain the only mutable tree-member store consumer'
|
||||
foreach ($sourceRoot in @('pages', 'components', 'utils')) {
|
||||
Get-ChildItem -LiteralPath (Join-Path $root $sourceRoot) -Recurse -File |
|
||||
Where-Object { $_.Extension -in @('.js', '.vue') -and $_.FullName -ne (Join-Path $root 'utils/api.js') } |
|
||||
ForEach-Object {
|
||||
$source = Get-Content -LiteralPath $_.FullName -Raw -Encoding utf8
|
||||
if ($source -match 'import\s*\{[^}]*\btreeMembers\b[^}]*\}\s*from\s*["'']@/data/mock\.js["'']') {
|
||||
throw "$($_.FullName) bypasses the tree-member snapshot selectors"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in @(
|
||||
@{ Key = 'T01'; Content = $treeOverview; Expected = 'listTreeMemberFixtures(genealogyId.value)' },
|
||||
@{ Key = 'T03'; Content = $t03; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, normalizedPersonId)' },
|
||||
@{ Key = 'T04'; Content = $pages['pages/tree/t04-add-relative.vue']; Expected = 'findTreeMemberFixture(genealogyId.value, personId.value)' },
|
||||
@{ Key = 'T05'; Content = $pages['pages/tree/t05-edit-member.vue']; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, id)' },
|
||||
@{ Key = 'T06'; Content = $t06; Expected = 'listTreeMemberFixtures(genealogyId.value)' },
|
||||
@{ Key = 'T07'; Content = $t07; Expected = 'listTreeMemberPresentationFixtures(genealogyId.value)' },
|
||||
@{ Key = 'T08'; Content = $t08; Expected = 'findTreeMemberPresentationFixture(genealogyId.value, personId.value)' }
|
||||
)) {
|
||||
Assert-Contains $entry.Content $entry.Expected "$($entry.Key) must scope fixture reads by genealogy and member identity"
|
||||
}
|
||||
|
||||
Assert-Contains $pages['pages/tree/t04-add-relative.vue'] 'const hasValidContext = computed(' 'T04 must own an explicit route-context gate'
|
||||
Assert-Contains $pages['pages/tree/t04-add-relative.vue'] 'isFirstMember.value ? !personId.value : Boolean(currentMember.value)' 'T04 must only allow a missing personId in first-member mode'
|
||||
Assert-Contains $pages['pages/tree/t05-edit-member.vue'] 'if (!member) return false;' 'T05 must reject unknown members instead of synthesizing an editable identity'
|
||||
Assert-Matches $pages['pages/tree/t05-edit-member.vue'] '(?s)\["privacy",\s*"forbidden"\]\.includes\(originalMember\.value\.status\).*?"no-permission"' 'T05 deep links must not edit a restricted member without a backend capability'
|
||||
|
||||
foreach ($asset in @('t01-state-panel.png', 't07-search-input-frame.png', 'list-slip-frame.png')) {
|
||||
Assert-Contains $profiles $asset "Adaptive tree profile missing: $asset"
|
||||
}
|
||||
|
||||
$catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Raw -Encoding utf8
|
||||
foreach ($entry in @('t04:', 't05:', 't06:', 't07:', 't08:')) {
|
||||
if ($catalog -match [regex]::Escape($entry)) { throw "Page catalog must remove migrated entry: $entry" }
|
||||
}
|
||||
|
||||
Write-Output 'T03-T08-MEMBER-FLOW-CONTRACT PASS'
|
||||
Write-Output 'T03-T08-MEMBER-FLOW-CONTRACT PASS NAVIGATION TRAIL LOCAL-PREVIEW'
|
||||
|
||||
Reference in New Issue
Block a user