147 lines
5.5 KiB
PowerShell
147 lines
5.5 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
|
|
function Assert-Contains {
|
|
param([string]$Content, [string]$Expected, [string]$Message)
|
|
if ($Content -notmatch [regex]::Escape($Expected)) { throw $Message }
|
|
}
|
|
|
|
function Assert-NoCssSurface {
|
|
param([string]$Content, [string]$ClassName)
|
|
foreach ($property in @('border', 'background', 'border-radius', 'box-shadow')) {
|
|
if ($Content -match "(?s)\.$ClassName\s*\{[^}]*\b$property\s*:") {
|
|
throw "T01 must not construct .$ClassName with CSS $property"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-NoPosition {
|
|
param([string]$Content, [string]$Selector)
|
|
$escaped = [regex]::Escape($Selector)
|
|
if ($Content -match "(?s)$escaped\s*\{[^}]*\bposition\s*:") {
|
|
throw "T01 ordinary layout selector must stay in document flow: $Selector"
|
|
}
|
|
}
|
|
|
|
$root = Split-Path -Parent $PSScriptRoot
|
|
$pagePath = Join-Path $root 'pages/tree/t01-tree-overview.vue'
|
|
$t02Path = Join-Path $root 'pages/tree/t02-tree-states.vue'
|
|
$page = Get-Content -LiteralPath $pagePath -Raw -Encoding utf8
|
|
$pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding utf8
|
|
|
|
if (Test-Path -LiteralPath $t02Path) { throw 'T02 duplicate route file must be removed' }
|
|
if ($pages -match 'pages/tree/t02-tree-states') { throw 'T02 duplicate route must be removed from pages.json' }
|
|
foreach ($required in @(
|
|
'appApi.getTree',
|
|
'createRequestController',
|
|
'isRequestCancelled',
|
|
'treeRequestController.abort()',
|
|
'onShow(() =>',
|
|
'requestController: treeRequestController'
|
|
)) { Assert-Contains $page $required "Missing T01 remote data contract: $required" }
|
|
|
|
foreach ($required in @(
|
|
'const treeState = ref("loading")',
|
|
'tree-state--tree',
|
|
'tree-state--landscape',
|
|
'tree-state--empty',
|
|
'tree-state--error',
|
|
'tree-canvas--state',
|
|
'lineage-connector',
|
|
'const generationRows = computed(',
|
|
'v-for="row in generationRows"',
|
|
':style="generationBandStyle(row)"',
|
|
'class="tree-stage"',
|
|
'tree-stage--lineage',
|
|
'class="generation-rail"',
|
|
'tree-scroll--lineage',
|
|
':style="nodeGridStyle(member)"',
|
|
'const nodeGridStyle = (member)',
|
|
'class="member-node__copy"',
|
|
't01-member-node-standard.png',
|
|
't01-member-node-selected.png',
|
|
't01-state-panel.png',
|
|
'class="member-action-profile"',
|
|
'const openMemberPanel = (member) =>',
|
|
'openPage("T03"',
|
|
'routeKey: "T04"',
|
|
'openPage("T06"',
|
|
'openPage("T07"',
|
|
'query.genealogyId',
|
|
'genealogyContext.isCurrentGenealogyInvalidated()',
|
|
':id="`tree-member-${member.id}`"',
|
|
':scroll-left="treeState === ''tree'' ? treeScrollLeft : 0"',
|
|
'@scroll="handleTreeScroll"',
|
|
'const currentTreeScrollLeft = ref(90)',
|
|
'uni.createSelectorQuery()',
|
|
'nodeRect.left + nodeRect.width / 2 - (scrollRect.left + scrollRect.width / 2)',
|
|
'const treeHasDrifted = ref(false)',
|
|
'const recenterSelectedMember = async () =>',
|
|
'class="tree-recenter"'
|
|
)) { Assert-Contains $page $required "Missing T01 contract: $required" }
|
|
|
|
foreach ($fixtureToken in @('getGenealogyFixtureAccess', 'treeMembers', 'mockTree')) {
|
|
if ($page -match [regex]::Escape($fixtureToken)) {
|
|
throw "T01 must not use a fixture as its remote tree source: $fixtureToken"
|
|
}
|
|
}
|
|
|
|
$invalidatedGuardIndex = $page.IndexOf('genealogyContext.isCurrentGenealogyInvalidated()')
|
|
$contextWriteIndex = $page.IndexOf('genealogyContext.setCurrentGenealogyId(genealogyId.value)')
|
|
if ($invalidatedGuardIndex -lt 0 -or $contextWriteIndex -le $invalidatedGuardIndex) {
|
|
throw 'T01 must reject an invalidated genealogy before any route can rewrite the current context'
|
|
}
|
|
|
|
foreach ($forbidden in @(
|
|
'g03-create-flow-panel.png',
|
|
'g06-search-input-wide.png',
|
|
'application-status-card.png',
|
|
'generation-band--twelve',
|
|
'generation-band--thirteen',
|
|
'generation-band--fourteen'
|
|
)) {
|
|
if ($page -match [regex]::Escape($forbidden)) { throw "T01 must not keep opaque surface asset: $forbidden" }
|
|
}
|
|
|
|
foreach ($className in @('tree-canvas', 'member-node', 'member-action-profile', 'tree-state-card')) {
|
|
Assert-NoCssSurface $page $className
|
|
}
|
|
|
|
foreach ($selector in @('.generation-rail', '.generation-band', '.member-node')) {
|
|
Assert-NoPosition $page $selector
|
|
}
|
|
|
|
if ($page -match '(?s)\.node-name,\s*\.node-relation,\s*\.node-years\s*\{[^}]*\bposition\s*:') {
|
|
throw 'T01 member node text must stay in document flow'
|
|
}
|
|
if ($page -notmatch '(?s)\.tree-page\s*\{[^}]*display:\s*grid;[^}]*grid-template-rows:\s*auto auto minmax\(0, 1fr\);') {
|
|
throw 'T01 page content must own a grid stacking context above the fixed module background'
|
|
}
|
|
|
|
foreach ($obsolete in @('const nodeStyle = (member)', 'const generationRowStyle = (row)', 'left: `${member.x}rpx`', 'top: `${member.y}rpx`')) {
|
|
if ($page -match [regex]::Escape($obsolete)) { throw "T01 obsolete positioned layout remains: $obsolete" }
|
|
}
|
|
|
|
foreach ($required in @(
|
|
'const treeMetrics = computed(',
|
|
':style="treeState === ''tree'' ? treeMetricsStyle : undefined"',
|
|
':style="generationBandStyle(row)"',
|
|
'overflow-y: auto;'
|
|
)) { Assert-Contains $page $required "T01 data-driven tree boundary missing: $required" }
|
|
if ($page -match '<scroll-view\s+class="tree-stage"') {
|
|
throw 'T01 outer vertical tree owner must preserve the stage grid instead of wrapping its grid children'
|
|
}
|
|
|
|
foreach ($fixedCapacity in @(
|
|
'grid-template-columns: repeat(180, 5rpx)',
|
|
'grid-template-rows: repeat(180, 5rpx)',
|
|
'width: 900rpx',
|
|
'height: 900rpx',
|
|
'grid-template-rows: 47rpx 58rpx 157rpx 58rpx 242rpx 58rpx 1fr'
|
|
)) {
|
|
if ($page -match [regex]::Escape($fixedCapacity)) {
|
|
throw "T01 must derive tree capacity from member data: $fixedCapacity"
|
|
}
|
|
}
|
|
|
|
Write-Output 'T01-TREE-STATE-CONTRACT PASS'
|