Review changes batch 6 of 6
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$component = Get-Content -LiteralPath (Join-Path $root 'components/AppLoading.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-Match -Content $component -Pattern ':class="`app-loading--\$\{variant\}`"' -Message 'AppLoading must expose page and section modifier classes'
|
||||
Assert-Match -Content $component -Pattern 'variant:\s*\{\s*type:\s*String,\s*default:\s*''page'',\s*validator:' -Message 'AppLoading must own a validated page/section variant prop'
|
||||
Assert-Match -Content $component -Pattern 'description:\s*\{\s*type:\s*String,\s*default:\s*''''\s*\}' -Message 'AppLoading must expose an optional description prop'
|
||||
Assert-Match -Content $component -Pattern 'v-if="description" class="app-loading__description"' -Message 'AppLoading must render its optional description'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading\s*\{[^}]*position:\s*relative;[^}]*z-index:\s*1;' -Message 'AppLoading must stay above page panel skins without page-specific overrides'
|
||||
Assert-Match -Content $component -Pattern 'class="app-loading__seal"\s+src="/static/assets/foundation/transparent/brand-seal\.png"' -Message 'AppLoading must render the approved real red-gold seal asset'
|
||||
Assert-Match -Content $component -Pattern 'class="app-loading__knot"\s+src="/static/assets/foundation/transparent/auth-divider-knot\.png"' -Message 'AppLoading must render the approved real gold knot asset'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s*\{[^}]*min-height:\s*320rpx;' -Message 'AppLoading page variant must use the approved minimum height'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'AppLoading page seal must use the approved size'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__copy\s*\{[^}]*font-size:\s*30rpx;' -Message 'AppLoading page copy must use the approved size'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--page\s+\.app-loading__description\s*\{[^}]*font-size:\s*24rpx;' -Message 'AppLoading page description must use the approved size'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s*\{[^}]*min-height:\s*180rpx;' -Message 'AppLoading section variant must use the approved minimum height'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__seal\s*\{[^}]*width:\s*88rpx;[^}]*height:\s*90rpx;' -Message 'AppLoading section seal must use the approved size'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__copy\s*\{[^}]*font-size:\s*24rpx;' -Message 'AppLoading section copy must use the approved size'
|
||||
Assert-Match -Content $component -Pattern '(?s)\.app-loading--section\s+\.app-loading__description\s*\{[^}]*font-size:\s*22rpx;' -Message 'AppLoading section description must use the approved size'
|
||||
Assert-Match -Content $component -Pattern 'app-loading-seal-breathe\s+1\.6s' -Message 'AppLoading seal must use the approved restrained breathing rhythm'
|
||||
Assert-Match -Content $component -Pattern '@media\s*\(prefers-reduced-motion:\s*reduce\)' -Message 'AppLoading must respect reduced-motion preferences'
|
||||
Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-seal-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential seal opacity pulse'
|
||||
Assert-Match -Content $component -Pattern '@keyframes\s+app-loading-knot-essential-pulse' -Message 'Reduced-motion AppLoading must retain an essential knot opacity pulse'
|
||||
Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__seal\s*\{[^}]*animation:\s*app-loading-seal-essential-pulse\s+1\.2s\s+ease-in-out\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion seal must pulse opacity without transform motion'
|
||||
Assert-Match -Content $component -Pattern '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?\.app-loading__knot\s*\{[^}]*animation:\s*app-loading-knot-essential-pulse\s+1\.2s\s+ease-in-out\s+\.2s\s+infinite;[^}]*transform:\s*none;' -Message 'Reduced-motion knot must pulse opacity without transform motion'
|
||||
if ($component -match '(?s)@media\s*\(prefers-reduced-motion:\s*reduce\).*?animation:\s*none') {
|
||||
throw 'Reduced-motion AppLoading must not become completely static'
|
||||
}
|
||||
|
||||
if ($component -match 'app-loading__mark|border:\s*4rpx\s+double') { throw 'AppLoading must not retain the legacy CSS box mark' }
|
||||
|
||||
Write-Output 'APP-LOADING-CONTRACT PASS'
|
||||
@@ -12,8 +12,8 @@ $captureScript = Get-Content -LiteralPath $captureScriptPath -Raw -Encoding utf8
|
||||
if ($captureScript -notmatch "const renderSettleMilliseconds = 900") {
|
||||
throw 'Chrome capture must define a 900ms render-settle delay after the target selector appears'
|
||||
}
|
||||
if ($captureScript -notmatch 'await sleep\(renderSettleMilliseconds\)') {
|
||||
throw 'Chrome capture must wait for the render-settle delay before taking a screenshot'
|
||||
if ($captureScript -notmatch "await sleep\(action\?\.endsWith\('-submitting'\) \? 80 : renderSettleMilliseconds\)") {
|
||||
throw 'Chrome capture must wait for the standard render-settle delay except for verified transient submitting states'
|
||||
}
|
||||
if ($captureScript -match 'await sleep\(300\)') {
|
||||
throw 'Chrome capture must not retain the unstable 300ms post-selector delay'
|
||||
@@ -27,10 +27,10 @@ if ($captureScript -notmatch 'Array\.from\(document\.images\)\.every') {
|
||||
if ($captureScript -notmatch 'await waitForLoadedImages\(send\)') {
|
||||
throw 'Chrome capture must wait for document images after the target selector renders'
|
||||
}
|
||||
if ($captureScript -notmatch "await send\('Page.reload'\)") {
|
||||
throw 'Chrome capture must reload after navigation so route screenshots do not inherit a prior Vue state'
|
||||
if ($captureScript -notmatch "await waitForPageLoad\(socket, \(\) => send\('Page.reload'\)\)") {
|
||||
throw 'Chrome capture must reload and await Page.loadEventFired so route screenshots do not inherit a prior Vue state'
|
||||
}
|
||||
if ($captureScript.IndexOf("await send('Page.navigate'") -ge $captureScript.IndexOf("await send('Page.reload')")) {
|
||||
if ($captureScript.IndexOf("await send('Page.navigate'") -ge $captureScript.IndexOf("await waitForPageLoad(socket, () => send('Page.reload'))")) {
|
||||
throw 'Chrome capture must reload only after it has navigated to the requested URL'
|
||||
}
|
||||
if ($captureScript -notmatch 'const waitForFreshDocument = async \(send, documentTimeOrigin\) =>') {
|
||||
@@ -45,7 +45,7 @@ if ($captureScript -notmatch 'await waitForFreshDocument\(send, documentTimeOrig
|
||||
if ($captureScript -notmatch "action !== 'fresh-navigation'") {
|
||||
throw 'Chrome capture must support fresh-navigation for root pages whose tab lifecycle closes a reloaded CDP target'
|
||||
}
|
||||
foreach ($g06Action in @('g06-results', 'g06-empty', 'g06-invite')) {
|
||||
foreach ($g06Action in @('g06-results', 'g06-empty', 'g06-error', 'g06-invite', 'g06-invite-invalid')) {
|
||||
if ($captureScript -notmatch [regex]::Escape($g06Action)) {
|
||||
throw "Chrome capture must support the $g06Action visual state"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @('prepareFSeriesState', 'f02-validation', 'module-preview-toast', 'waitForPageLoad')) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "F series capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareFSeriesState\(send, action\)') { throw 'Capture runner does not prepare F series states' }
|
||||
if ($capture -notmatch "waitForPageLoad\(socket, \(\) => send\('Page\.reload'\)\)") { throw 'Capture runner must wait for the reload load event before DOM inspection' }
|
||||
|
||||
Write-Output 'CAPTURE-F-SERIES-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'prepareG03State',
|
||||
'g03-create-validation',
|
||||
'g03-duplicate-reminder',
|
||||
'g03-create-submitting',
|
||||
'g03-create-error',
|
||||
'g03-ancestor-validation',
|
||||
'g03-ancestor-submitting',
|
||||
'g03-ancestor-error',
|
||||
'g03-ancestor-success'
|
||||
)) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "G03 capture state missing: $required" }
|
||||
}
|
||||
|
||||
if ($capture -notmatch 'await prepareG03State\(send, action\)') { throw 'Capture runner does not prepare G03 states' }
|
||||
if ($capture -notmatch 'action\?\.endsWith\(''-submitting''\)') { throw 'Capture runner does not shorten settle time for transient submitting states' }
|
||||
|
||||
Write-Output 'CAPTURE-G03-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,8 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
foreach ($required in @('prepareG08State', 'g08-validation', 'g08-submitting', 'g08-error', 'g08-success')) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "G08 capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareG08State\(send, action\)') { throw 'Capture runner does not prepare G08 states' }
|
||||
Write-Output 'CAPTURE-G08-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,17 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'prepareG10State',
|
||||
'g10-help',
|
||||
'g10-approve-dialog',
|
||||
'g10-approved',
|
||||
'g10-reject-dialog',
|
||||
'g10-rejected'
|
||||
)) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "G10 capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareG10State\(send, action\)') { throw 'Capture runner does not prepare G10 states' }
|
||||
|
||||
Write-Output 'CAPTURE-G10-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,14 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'prepareG11State', 'g11-validation', 'g11-saved',
|
||||
'prepareG12State', 'g12-validation', 'g12-save-error', 'g12-saved'
|
||||
)) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "G11/G12 capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareG11State\(send, action\)') { throw 'Capture runner does not prepare G11 states' }
|
||||
if ($capture -notmatch 'await prepareG12State\(send, action\)') { throw 'Capture runner does not prepare G12 states' }
|
||||
|
||||
Write-Output 'CAPTURE-G11-G12-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,10 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @('prepareT03T06State', 't06-conflict-help')) {
|
||||
if ($capture -notmatch [regex]::Escape($required)) { throw "T03-T06 capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareT03T06State\(send, action\)') { throw 'Capture runner does not prepare T03-T06 states' }
|
||||
|
||||
Write-Output 'CAPTURE-T03-T06-STATES-CONTRACT PASS'
|
||||
@@ -53,11 +53,12 @@ if ($pages -match 'pages/genealogy/g04-first-ancestor') {
|
||||
throw 'G04 first-person route must be merged into G03'
|
||||
}
|
||||
$createFlow = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue')
|
||||
foreach ($token in @('step=ancestor', 'createPerson', 'genealogyContext')) {
|
||||
foreach ($token in @('step=ancestor', 'submitAncestor', 'genealogyId')) {
|
||||
if ($createFlow -notmatch [regex]::Escape($token)) {
|
||||
throw "Missing G03 first-person flow: $token"
|
||||
}
|
||||
}
|
||||
if ($createFlow -match 'createPerson') { throw 'G03 must not retain the removed createPerson entrypoint' }
|
||||
foreach ($route in @('pages/family/f04-article-list', 'pages/family/f02-publish-feed')) {
|
||||
if ($pages -notmatch [regex]::Escape($route)) {
|
||||
throw "Missing content page route: $route"
|
||||
@@ -69,7 +70,11 @@ foreach ($pageFile in @('pages/genealogy/g05-genealogy-overview.vue', 'pages/tre
|
||||
if ($page -match "id=1001|id=3") {
|
||||
throw "Hard-coded route ID remains in $pageFile"
|
||||
}
|
||||
if ($page -notmatch 'genealogyContext') {
|
||||
if ($pageFile -eq 'pages/genealogy/g05-genealogy-overview.vue') {
|
||||
if ($page -notmatch "const genealogyId = ref\(''\)" -or $page -notmatch 'query\.genealogyId') {
|
||||
throw 'G05 must own its explicit genealogyId route context'
|
||||
}
|
||||
} elseif ($page -notmatch 'genealogyContext') {
|
||||
throw "Missing genealogy context in $pageFile"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Read-Utf8([string]$Path) { [System.IO.File]::ReadAllText((Join-Path (Join-Path $PSScriptRoot '..') $Path), [System.Text.Encoding]::UTF8) }
|
||||
function Assert-Contains([string]$Content, [string]$Expected, [string]$Message) { if (-not $Content.Contains($Expected)) { throw $Message } }
|
||||
|
||||
$f01 = Read-Utf8 'pages/family/f01-family-feed.vue'
|
||||
$f02 = Read-Utf8 'pages/family/f02-publish-feed.vue'
|
||||
$module = Read-Utf8 'components/ModulePage.vue'
|
||||
|
||||
foreach ($expected in @('font-size:24rpx','font-size:23rpx','font-size:22rpx')) {
|
||||
Assert-Contains $f01 $expected "F01 readability token missing: $expected"
|
||||
}
|
||||
foreach ($expected in @('font-size:24rpx','font-size:23rpx')) {
|
||||
Assert-Contains $f02 $expected "F02 readability token missing: $expected"
|
||||
}
|
||||
foreach ($expected in @(
|
||||
'.module-lead { position: relative; height: 56rpx; margin: 0 12rpx 18rpx; color: $ink-muted; font-size: 23rpx;',
|
||||
'.form-row > text { position: absolute; top: 28rpx; left: 26rpx; z-index: 1; color: $ink; font-size: 24rpx;',
|
||||
'.form-row input { position: absolute; top: 0; right: 22rpx; bottom: 0; left: 178rpx; z-index: 1; height: 84rpx; color: $ink; font-size: 23rpx;',
|
||||
'.list-card__copy text:nth-child(2) { margin-top: 10rpx; color: $ink-muted; font-size: 23rpx;',
|
||||
'.detail-card text:last-child { margin-top: 10rpx; color: $ink; font-size: 24rpx;',
|
||||
'.status-card__note { display: block; margin-top: 17rpx; color: $ink-muted; font-size: 24rpx;'
|
||||
)) {
|
||||
Assert-Contains $module $expected "ModulePage readability contract missing: $expected"
|
||||
}
|
||||
|
||||
Write-Output 'F series all-states visual contract passed.'
|
||||
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/family/f01-family-feed.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match([string]$Pattern, [string]$Message) {
|
||||
if ($page -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-Match 'modules/family/transparent/f01-family-letter-card\.png' 'F01 must use its dedicated transparent family-letter card.'
|
||||
if ($page -match 'application-status-card\.png') { throw 'F01 must not reuse the application status card.' }
|
||||
Assert-Match 'class="feed-card__title"' 'F01 must expose an explicit feed title hierarchy.'
|
||||
Assert-Match 'class="feed-card__meta"' 'F01 must expose category and time as secondary metadata.'
|
||||
Assert-Match 'class="feed-card__summary"' 'F01 must expose feed summary copy.'
|
||||
Assert-Match 'class="feed-card__author"' 'F01 must expose the author as tertiary information.'
|
||||
Assert-Match '(?s)\.feed-shortcut\s*\{[^}]*min-height:\s*44px;' 'F01 shortcuts must preserve a 44 CSS px touch height.'
|
||||
Assert-Match 'class="feed-shortcuts__skin"' 'F01 shortcuts must share one visible navigation skin.'
|
||||
Assert-Match '(?s)\.feed-shortcuts\s*\{[^}]*grid-template-columns:\s*repeat\(4,minmax\(0,1fr\)\);' 'F01 shortcuts must use one four-column equal-width navigation strip.'
|
||||
|
||||
Write-Output 'F01-MODULE-BASELINE-CONTRACT PASS'
|
||||
@@ -0,0 +1,40 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
function Read-Page([string]$Path) {
|
||||
Get-Content -LiteralPath (Join-Path $root $Path) -Raw -Encoding utf8
|
||||
}
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
$pages = @{
|
||||
G05 = Read-Page 'pages/genealogy/g05-genealogy-overview.vue'
|
||||
G06 = Read-Page 'pages/genealogy/g06-search-genealogies.vue'
|
||||
G09 = Read-Page 'pages/genealogy/g09-my-applications.vue'
|
||||
G10 = Read-Page 'pages/genealogy/g10-application-review.vue'
|
||||
G11 = Read-Page 'pages/genealogy/g11-genealogy-settings.vue'
|
||||
G12 = Read-Page 'pages/genealogy/g12-generation-poems.vue'
|
||||
}
|
||||
|
||||
foreach ($entry in $pages.GetEnumerator()) {
|
||||
Assert-Match -Content $entry.Value -Pattern 'import AppLoading from [''"]@/components/AppLoading\.vue[''"]' -Message "$($entry.Key) must import AppLoading"
|
||||
}
|
||||
|
||||
Assert-Match -Content $pages.G05 -Pattern '(?s)<view\s+v-else-if="overviewState === ''loading''"\s+class="overview-state overview-state--loading">.*?<AppLoading[^>]+description="[^"]+"\s*/>.*?</view>' -Message 'G05 must render AppLoading inside its transparent framed loading state'
|
||||
Assert-Match -Content $pages.G06 -Pattern '<AppLoading\s+v-if="searchState === ''loading''"\s+variant="section"[^>]+description="[^"]+"\s*/>' -Message 'G06 search results must render the section AppLoading variant'
|
||||
Assert-Match -Content $pages.G06 -Pattern 'requestedState === ''loading''' -Message 'G06 must expose its section loading state for visual review'
|
||||
Assert-Match -Content $pages.G09 -Pattern '<AppLoading\s+v-else-if="applicationState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G09 must render a page AppLoading branch'
|
||||
Assert-Match -Content $pages.G10 -Pattern '<AppLoading\s+v-else-if="reviewState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G10 must render a page AppLoading branch'
|
||||
Assert-Match -Content $pages.G11 -Pattern '<AppLoading\s+v-if="settingsState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G11 must render a page AppLoading branch'
|
||||
Assert-Match -Content $pages.G12 -Pattern '<AppLoading\s+v-if="poemState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'G12 must render a page AppLoading branch'
|
||||
Assert-Match -Content $pages.G10 -Pattern 'if \(query\.state === ''loading''\) \{ reviewState\.value = ''loading''; return \}' -Message 'G10 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.G11 -Pattern 'query\.state === ''loading'' \? ''loading''' -Message 'G11 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.G12 -Pattern 'query\.state === ''loading'' \? ''loading''' -Message 'G12 must preserve its loading presentation state'
|
||||
|
||||
if ($pages.G06 -match 'class="search-status search-loading"') { throw 'G06 must not retain its duplicated text-only search loader' }
|
||||
|
||||
Write-Output 'G-SERIES-APP-LOADING-CONTRACT PASS'
|
||||
@@ -21,6 +21,9 @@ $paths = @($pages.pages.path)
|
||||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||||
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g01-empty-state-runtime-smoke.js') -Raw -Encoding utf8
|
||||
$panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g01-empty-panel.png'
|
||||
$framePath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
|
||||
$pipelinePackage = Get-Content -LiteralPath (Join-Path $root 'design-pipeline/package.json') -Raw -Encoding utf8
|
||||
$frameBuilderPath = Join-Path $root 'design-pipeline/scripts/build-g01-empty-frame.mjs'
|
||||
|
||||
if (-not ($paths -contains 'pages/genealogy/g01-my-genealogies')) { throw 'G01 route missing' }
|
||||
if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route must be removed; empty state belongs to G01' }
|
||||
@@ -28,11 +31,12 @@ if ($paths -contains 'pages/genealogy/g02-empty-genealogies') { throw 'G02 route
|
||||
foreach ($required in @(
|
||||
'forceEmptyState',
|
||||
'query.get("state") || "default"',
|
||||
'g01-empty-panel.png',
|
||||
'empty-create-action',
|
||||
'empty-search-action',
|
||||
'empty-invite-action',
|
||||
'empty-create-note',
|
||||
'empty-panel__frame',
|
||||
'g01-empty-panel-frame.png',
|
||||
'const createGenealogy = () =>',
|
||||
'/pages/genealogy/g03-create-genealogy',
|
||||
'/pages/genealogy/g06-search-genealogies',
|
||||
@@ -56,12 +60,32 @@ foreach ($required in @(
|
||||
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-index--split\s*\{[^}]*height:\s*100vh;[^}]*padding-bottom:\s*calc\(112rpx\s*\+\s*env\(safe-area-inset-bottom\)\)' -Message 'G01 split layout must reserve the fixed tabbar and safe area'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-list-scroll\s*\{[^}]*height:\s*0;[^}]*flex:\s*1;' -Message 'G01 list scroll region must consume the remaining computed height'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)<view v-else class="genealogy-empty-state">.*?<view class="empty-panel">\s*<image\s+class="empty-panel__frame"\s+src="/static/assets/modules/genealogy/transparent/g01-empty-panel-frame\.png"\s+mode="scaleToFill"\s*/>' -Message 'G01 empty state must render the exact extracted transparent frame without an opaque panel skin'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__frame\s*\{[^}]*position:\s*absolute;[^}]*inset:\s*0;[^}]*width:\s*100%;[^}]*height:\s*100%;[^}]*pointer-events:\s*none;' -Message 'G01 empty state transparent frame does not cover the approved panel slot'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.genealogy-empty-state\s*\{[^}]*min-height:\s*1120rpx;' -Message 'G01 empty state does not reserve the approved visibly fuller height'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 empty panel does not use the approved visibly fuller height'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-panel__content\s*\{[^}]*justify-content:\s*center;[^}]*padding:\s*50rpx 28rpx 46rpx;' -Message 'G01 empty panel content is not centered with the approved responsive padding'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-seal\s*\{[^}]*width:\s*120rpx;[^}]*height:\s*184rpx;' -Message 'G01 empty seal does not use the approved prominent size'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-title\s*\{[^}]*margin-top:\s*30rpx;[^}]*font-size:\s*48rpx;' -Message 'G01 empty title does not use the approved prominent size and spacing'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-copy\s*\{[^}]*min-height:\s*84rpx;[^}]*margin-top:\s*18rpx;[^}]*font-size:\s*28rpx;' -Message 'G01 empty guidance does not use the approved prominent size and spacing'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action,\s*\.empty-invite-action\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 empty actions do not use the approved prominent size'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-search-action__copy,\s*\.empty-invite-action__copy\s*\{[^}]*font-size:\s*34rpx;' -Message 'G01 empty action labels do not use the approved prominent size'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action\s*\{[^}]*min-height:\s*76rpx;[^}]*margin-top:\s*14rpx;' -Message 'G01 empty create action does not use the approved prominent hit area and spacing'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-action__copy\s*\{[^}]*font-size:\s*31rpx;' -Message 'G01 empty create action does not use the approved prominent size'
|
||||
if ($g01 -match '(?s)\.empty-create-action__copy\s*\{[^}]*text-decoration:\s*underline') {
|
||||
throw 'G01 empty create action must not render a text underline'
|
||||
}
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.empty-create-note\s*\{[^}]*font-size:\s*26rpx;[^}]*line-height:\s*38rpx;' -Message 'G01 empty-state create note does not use the approved readable size'
|
||||
if ($g01 -match '(?s)<view v-else class="genealogy-empty-state">.*?<image[^>]+g01-empty-panel\.png') { throw 'G01 empty state must not retain the opaque panel background' }
|
||||
|
||||
foreach ($className in @('empty-panel', 'empty-create-action', 'empty-search-action')) {
|
||||
Assert-NoCssSurface -Content $g01 -ClassName $className
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G01 requires its final raster empty-panel asset' }
|
||||
if (-not (Test-Path -LiteralPath $framePath)) { throw 'G01 requires the extracted transparent empty-panel frame asset' }
|
||||
if (-not (Test-Path -LiteralPath $frameBuilderPath)) { throw 'G01 transparent empty-panel frame must remain reproducible from its builder' }
|
||||
Assert-Match -Content $pipelinePackage -Pattern '"build:g01-empty-frame"\s*:\s*"node scripts/build-g01-empty-frame\.mjs"' -Message 'G01 transparent frame builder is not exposed by the design pipeline'
|
||||
Assert-Match -Content $runtimeSmoke -Pattern '\{\s*width:\s*412,\s*height:\s*1000\s*\}' -Message 'G01 runtime smoke must include the approved 412x1000 tall-screen stress viewport'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$panel = [System.Drawing.Bitmap]::FromFile($panelPath)
|
||||
@@ -78,4 +102,21 @@ try {
|
||||
$panel.Dispose()
|
||||
}
|
||||
|
||||
$frame = [System.Drawing.Bitmap]::FromFile($framePath)
|
||||
try {
|
||||
if ($frame.Width -ne 1122 -or $frame.Height -ne 1402) { throw 'G01 transparent empty-panel frame must remain 1122 x 1402px' }
|
||||
foreach ($point in @(@(0, 0), @(1121, 0), @(0, 1401), @(1121, 1401), @(561, 701))) {
|
||||
if ($frame.GetPixel($point[0], $point[1]).A -ne 0) { throw 'G01 transparent empty-panel frame retains an opaque background pixel' }
|
||||
}
|
||||
$visible = 0
|
||||
for ($y = 0; $y -lt $frame.Height; $y += 4) {
|
||||
for ($x = 0; $x -lt $frame.Width; $x += 4) {
|
||||
if ($frame.GetPixel($x, $y).A -gt 0) { $visible++ }
|
||||
}
|
||||
}
|
||||
if ($visible -lt 1200 -or $visible -gt 12000) { throw "G01 transparent frame alpha coverage is implausible: $visible sampled pixels" }
|
||||
} finally {
|
||||
$frame.Dispose()
|
||||
}
|
||||
|
||||
Write-Output 'G01-EMPTY-STATE-CONTRACT PASS'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
$errorBlock = [regex]::Match($g01, '(?s)<view v-else-if="hasError" class="state-panel state-panel--error">.*?<template v-else-if="hasGenealogies">').Value
|
||||
if (-not $errorBlock) { throw 'G01 error state block missing' }
|
||||
foreach ($required in @('error-panel__frame', 'g01-empty-panel-frame.png', 'error-panel__seal', 'brand-seal.png', 'error-panel__divider', 'section-divider.png', 'state-retry', 'a01-scroll-primary-v3.png', '@click="retryLoad"')) {
|
||||
if ($errorBlock -notmatch [regex]::Escape($required)) { throw "G01 error state missing approved contract: $required" }
|
||||
}
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s*\{[^}]*height:\s*1120rpx;' -Message 'G01 error state must use the approved full-height frame slot'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.error-panel__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' -Message 'G01 error state seal must use the approved prominent size'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-title\s*\{[^}]*font-size:\s*42rpx;' -Message 'G01 error title must use the approved hierarchy'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-copy\s*\{[^}]*font-size:\s*27rpx;' -Message 'G01 error guidance must use the approved readable size'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)\.state-panel--error\s+\.state-retry\s*\{[^}]*width:\s*560rpx;[^}]*height:\s*124rpx;' -Message 'G01 retry action must match the approved primary action size'
|
||||
|
||||
if ($errorBlock -match 'modules/genealogy/opaque/g01-empty-panel\.png') { throw 'G01 error state must not retain the legacy opaque panel' }
|
||||
|
||||
Write-Output 'G01-ERROR-STATE-CONTRACT PASS'
|
||||
@@ -0,0 +1,20 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||||
$appLoading = Get-Content -LiteralPath (Join-Path $root 'components/AppLoading.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-Match -Content $g01 -Pattern 'import AppLoading from "@/components/AppLoading\.vue";' -Message 'G01 loading state must import the global AppLoading component'
|
||||
Assert-Match -Content $g01 -Pattern '(?s)<view v-if="isLoading" class="state-panel state-panel--loading">\s*<AppLoading\s+text="[^"]+"\s+description="[^"]+"\s*/>\s*</view>' -Message 'G01 loading state must pass its approved title and supporting copy directly to AppLoading'
|
||||
Assert-Match -Content $appLoading -Pattern 'class="app-loading__seal"\s+src="/static/assets/foundation/transparent/brand-seal\.png"' -Message 'Global AppLoading must retain its approved red-gold seal asset'
|
||||
Assert-Match -Content $appLoading -Pattern 'app-loading-seal-breathe' -Message 'Global AppLoading must retain its restrained seal breathing motion'
|
||||
|
||||
if ($g01 -match 'loading-seal') { throw 'G01 loading state must not retain the legacy hollow loading seal' }
|
||||
if ($g01 -match 'state-copy--loading') { throw 'G01 loading state must not duplicate AppLoading description layout' }
|
||||
|
||||
Write-Output 'G01-LOADING-STATE-CONTRACT PASS'
|
||||
@@ -18,7 +18,7 @@ $pages = Get-Content -LiteralPath (Join-Path $root 'pages.json') -Raw -Encoding
|
||||
$paths = @($pages.pages.path)
|
||||
$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
|
||||
$g04 = Join-Path $root 'pages/genealogy/g04-first-ancestor.vue'
|
||||
$panelPath = Join-Path $root 'static/assets/modules/genealogy/opaque/g03-create-flow-panel.png'
|
||||
$panelPath = Join-Path $root 'static/assets/modules/genealogy/transparent/g01-empty-panel-frame.png'
|
||||
|
||||
if (-not ($paths -contains 'pages/genealogy/g03-create-genealogy')) { throw 'G03 route missing' }
|
||||
if ($paths -contains 'pages/genealogy/g04-first-ancestor') { throw 'G04 route must be removed; first-ancestor is a G03 state' }
|
||||
@@ -36,7 +36,7 @@ foreach ($required in @(
|
||||
'class="duplicate-reminder-layer"',
|
||||
'if (isSubmitting.value) return;',
|
||||
'/pages/genealogy/g05-genealogy-overview?genealogyId=',
|
||||
'g03-create-flow-panel.png',
|
||||
'g01-empty-panel-frame.png',
|
||||
'root-header-cinnabar.jpg',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'create-flow-panel',
|
||||
@@ -53,18 +53,18 @@ foreach ($className in @('create-flow-panel', 'flow-primary-action')) {
|
||||
Assert-NoCssSurface -Content $g03 -ClassName $className
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G03 requires a final opaque flow-panel bitmap' }
|
||||
if (-not (Test-Path -LiteralPath $panelPath)) { throw 'G03 requires the accepted genealogy paper panel bitmap' }
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
$panel = [System.Drawing.Bitmap]::FromFile($panelPath)
|
||||
try {
|
||||
if ($panel.Width -ne 1122 -or $panel.Height -ne 1500) { throw 'G03 flow-panel asset must be 1122 x 1500px' }
|
||||
if ($panel.Width -ne 1122 -or $panel.Height -ne 1402) { throw 'G03 paper panel asset must be 1122 x 1402px' }
|
||||
$corners = @(
|
||||
$panel.GetPixel(0, 0).A,
|
||||
$panel.GetPixel(($panel.Width - 1), 0).A,
|
||||
$panel.GetPixel(0, ($panel.Height - 1)).A,
|
||||
$panel.GetPixel(($panel.Width - 1), ($panel.Height - 1)).A
|
||||
)
|
||||
if (($corners | Where-Object { $_ -ne 255 }).Count -ne 0) { throw 'G03 flow-panel outer corners must be opaque' }
|
||||
if (($corners | Where-Object { $_ -ne 0 }).Count -ne 0) { throw 'G03 paper panel outer corners must remain transparent' }
|
||||
} finally {
|
||||
$panel.Dispose()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$g03 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g03-create-genealogy.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
if ($g03 -match '<text>返回</text>') { throw 'G03 header must use only the approved image back arrow' }
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back,\s*\.flow-header__side\s*\{[^}]*width:\s*72rpx;' -Message 'G03 header side slots must preserve centered title after removing back text'
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.flow-header__back-icon\s*\{[^}]*width:\s*34rpx;[^}]*height:\s*34rpx;' -Message 'G03 back arrow must use the approved visible size'
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.flow-note,\s*\.flow-rule__note\s*\{[^}]*font-size:\s*25rpx;' -Message 'G03 guidance copy must use the approved readable size'
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.flow-rule__option\s*\{[^}]*font-size:\s*23rpx;' -Message 'G03 visibility options must use the approved readable size'
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;' -Message 'G03 field errors must use the approved readable size'
|
||||
Assert-Match -Content $g03 -Pattern '(?s)\.flow-error\s*\{[^}]*font-size:\s*25rpx;[^}]*line-height:\s*36rpx;' -Message 'G03 submit errors must use the approved readable size'
|
||||
|
||||
foreach ($asset in @('a01-scroll-dialog-v3.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png')) {
|
||||
if ($g03 -notmatch [regex]::Escape($asset)) { throw "G03 must retain approved shared asset: $asset" }
|
||||
}
|
||||
|
||||
Write-Output 'G03-VISUAL-STATES-CONTRACT PASS'
|
||||
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-overview.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match { param([string]$Pattern,[string]$Message); if ($g05 -notmatch $Pattern) { throw $Message } }
|
||||
|
||||
Assert-Match 'overview-surface--state' 'G05 system states must own an independent surface'
|
||||
Assert-Match 'class="overview-state__frame".*g01-empty-panel-frame\.png' 'G05 system states must reuse the transparent gold frame'
|
||||
Assert-Match 'class="overview-state__seal".*brand-seal\.png' 'G05 non-loading states must use the approved red-gold seal'
|
||||
Assert-Match '(?s)\.overview-surface--state\s+\.overview-surface__skin\s*\{[^}]*display:\s*none;' 'G05 system states must hide the legacy function-grid skin'
|
||||
Assert-Match '(?s)\.overview-state__seal\s*\{[^}]*width:\s*132rpx;[^}]*height:\s*136rpx;' 'G05 state seal must use the approved size'
|
||||
Assert-Match 'overview-action-lock' 'G05 member mode must explain owner-only actions instead of leaving blank cells'
|
||||
Assert-Match '(?s)\.overview-actions--member\s*\{[^}]*grid-template-rows:\s*1fr 1fr;' 'G05 member actions must retain the full two-row surface'
|
||||
Assert-Match '(?s)\.overview-public__details > view:nth-child\(5\)\s*\{[^}]*grid-column:\s*1 / -1;' 'G05 public fifth identity detail must span the full row'
|
||||
Assert-Match '(?s)\.overview-public__details > view:nth-child\(5\)\s*\{[^}]*position:\s*absolute;[^}]*top:\s*250rpx;' 'G05 public parent genealogy must occupy the dedicated identity row'
|
||||
Assert-Match '(?s)</view>\s*<view class="overview-public__notice">.*publicDescription.*</view>\s*<view class="overview-public__action"' 'G05 public description must occupy the dedicated bottom note surface'
|
||||
Assert-Match '(?s)\.overview-public__notice\s*\{[^}]*position:\s*absolute;[^}]*top:\s*78%;' 'G05 public description must stay inside the approved bottom note region'
|
||||
|
||||
Write-Output 'G05-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -17,6 +17,7 @@ function Assert-NoCssSurface {
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$g01 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g01-my-genealogies.vue') -Raw -Encoding utf8
|
||||
$g05 = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g05-genealogy-overview.vue') -Raw -Encoding utf8
|
||||
$runtimeSmoke = Get-Content -LiteralPath (Join-Path $root 'tests/g05-overview-runtime-smoke.js') -Raw -Encoding utf8
|
||||
$asset = Join-Path $root 'static/assets/modules/genealogy/opaque/g05-overview-surface.png'
|
||||
|
||||
Assert-Contains $g01 'g05-genealogy-overview?genealogyId=' 'G01 must open G05 with the genealogyId query contract'
|
||||
@@ -53,5 +54,6 @@ foreach ($className in @('overview-surface', 'overview-action', 'overview-state-
|
||||
Assert-NoCssSurface $g05 $className
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $asset)) { throw 'G05 requires final opaque asset: g05-overview-surface.png' }
|
||||
Assert-Contains $runtimeSmoke 'waitForPageLoad' 'G05 runtime smoke must wait for the reload load event before DOM inspection'
|
||||
|
||||
Write-Output 'G05-OVERVIEW-CONTRACT PASS'
|
||||
|
||||
@@ -42,12 +42,31 @@ const waitFor = async (send, expression, message) => {
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
const open = async (send, query, selector) => {
|
||||
const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
socket.removeEventListener('message', onMessage)
|
||||
reject(new Error('G05 reload did not emit Page.loadEventFired'))
|
||||
}, 4000)
|
||||
const onMessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
if (message.method !== 'Page.loadEventFired') return
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener('message', onMessage)
|
||||
resolve()
|
||||
}
|
||||
socket.addEventListener('message', onMessage)
|
||||
Promise.resolve().then(trigger).catch((error) => {
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener('message', onMessage)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
const open = async (socket, send, query, selector) => {
|
||||
const url = `${origin}/#${route}${query}`
|
||||
await send('Page.navigate', { url })
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, 'G05 navigation failed')
|
||||
const documentTimeOrigin = await valueOf(send, 'performance.timeOrigin')
|
||||
await send('Page.reload')
|
||||
await waitForPageLoad(socket, () => send('Page.reload'))
|
||||
await waitFor(send, `performance.timeOrigin !== ${JSON.stringify(documentTimeOrigin)}`, `G05 ${query} did not reload into a fresh document`)
|
||||
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `G05 ${query} did not render`)
|
||||
}
|
||||
@@ -57,27 +76,27 @@ const run = async () => {
|
||||
try {
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
await open(send, '?state=empty', '.overview-state--empty')
|
||||
await open(send, '?state=error&genealogyId=1001', '.overview-state--error')
|
||||
await open(send, '?state=no-permission&genealogyId=1001', '.overview-state--no-permission')
|
||||
await open(send, '?mode=public&genealogyId=2001', '.overview-public')
|
||||
await open(socket, send, '?state=empty', '.overview-state--empty')
|
||||
await open(socket, send, '?state=error&genealogyId=1001', '.overview-state--error')
|
||||
await open(socket, send, '?state=no-permission&genealogyId=1001', '.overview-state--no-permission')
|
||||
await open(socket, send, '?mode=public&genealogyId=2001', '.overview-public')
|
||||
if ((await valueOf(send, "document.querySelectorAll('.overview-public__details > uni-view').length")) !== 5) throw new Error('G05 public preview is missing identity details')
|
||||
if (await valueOf(send, "Boolean(document.querySelector('.overview-actions'))")) throw new Error('G05 public preview exposed member actions')
|
||||
await valueOf(send, "document.querySelector('.overview-public__action').click()")
|
||||
await waitFor(send, "location.href.includes('/pages/genealogy/g08-join-application?source=search&genealogyId=2001')", 'G05 public apply action did not open G08 search source')
|
||||
|
||||
await open(send, '?role=member&genealogyId=1001', '.overview-ready')
|
||||
await open(socket, send, '?role=member&genealogyId=1001', '.overview-ready')
|
||||
if ((await valueOf(send, "document.querySelectorAll('.overview-action').length")) !== 2) throw new Error('G05 member overview exposed owner actions')
|
||||
if (await valueOf(send, "document.querySelector('.header-action')?.textContent.trim().length > 0")) throw new Error('G05 member overview exposed management action')
|
||||
|
||||
await open(send, '?genealogyId=1001', '.overview-ready')
|
||||
await open(socket, send, '?genealogyId=1001', '.overview-ready')
|
||||
if ((await valueOf(send, "document.querySelectorAll('.overview-action').length")) !== 4) throw new Error('G05 owner overview is missing management actions')
|
||||
await valueOf(send, "document.querySelector('.overview-action--ancestor')?.click()")
|
||||
await waitFor(send, "location.href.includes('/pages/genealogy/g03-create-genealogy?step=ancestor&genealogyId=1001')", 'G05 ancestor action did not open the G03 ancestor step')
|
||||
|
||||
for (const size of [{ width: 320, height: 568 }, { width: 360, height: 800 }, { width: 412, height: 915 }]) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true })
|
||||
await open(send, '?mode=public&genealogyId=2001', '.overview-public')
|
||||
await open(socket, send, '?mode=public&genealogyId=2001', '.overview-public')
|
||||
const width = await valueOf(send, 'document.documentElement.scrollWidth')
|
||||
if (width > size.width + 1) throw new Error(`G05 has horizontal overflow at ${size.width}x${size.height}`)
|
||||
}
|
||||
|
||||
@@ -78,6 +78,11 @@ foreach ($className in @('search-title-strip', 'search-field', 'search-action'))
|
||||
if ($g06 -notmatch '(?s)\.search-controls\s*\{[^}]*display:\s*flex[^}]*align-items:\s*center[^}]*gap:\s*20rpx') {
|
||||
throw 'G06 search input and action must form one compact control row'
|
||||
}
|
||||
Assert-Contains -Content $g06 -Expected 'class="status-retry"' -Message 'G06 error retry action is missing'
|
||||
Assert-Contains -Content $g06 -Expected 'src="/static/assets/foundation/transparent/a01-scroll-primary-v3.png"' -Message 'G06 error retry must use the approved A-series primary scroll asset'
|
||||
if ($g06 -notmatch '(?s)\.status-retry\s*\{[^}]*width:\s*300rpx;[^}]*height:\s*76rpx;') {
|
||||
throw 'G06 error retry must use the approved compact action size'
|
||||
}
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
foreach ($asset in @(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g09-my-applications.vue') -Raw -Encoding utf8
|
||||
|
||||
$rules = @(
|
||||
'(?s)\.application-intro text:last-child\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.application-card__time\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.application-card__relation\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.application-card__status\s*\{[^}]*font-size:\s*25rpx;',
|
||||
'(?s)\.application-card__hint\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.application-card__action\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.application-state-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)
|
||||
foreach ($rule in $rules) {
|
||||
if ($page -notmatch $rule) { throw "G09 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'G09-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -0,0 +1,31 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/genealogy/g10-application-review.vue') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'class="review-result"',
|
||||
'review-result--approved',
|
||||
'review-result--rejected',
|
||||
'a01-secondary-button-v2.png',
|
||||
'a01-primary-button-v2.png',
|
||||
'a01-scroll-secondary-v3.png',
|
||||
'a01-scroll-primary-v3.png'
|
||||
)) {
|
||||
if ($page -notmatch [regex]::Escape($required)) { throw "G10 result visual missing: $required" }
|
||||
}
|
||||
|
||||
$rules = @(
|
||||
'(?s)\.review-intro text:nth-child\(2\)\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.application-card__phone\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.application-card__time\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.application-card__relation\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.review-action\s*\{[^}]*width:\s*148rpx;[^}]*height:\s*68rpx;',
|
||||
'(?s)\.review-action text\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.review-result\s*\{[^}]*width:\s*308rpx;[^}]*height:\s*68rpx;',
|
||||
'(?s)\.review-state-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)
|
||||
foreach ($rule in $rules) {
|
||||
if ($page -notmatch $rule) { throw "G10 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'G10-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -0,0 +1,26 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$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
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.settings-form__copy, \.settings-result__copy\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.settings-field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;',
|
||||
'(?s)\.visibility-block__hint\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.visibility-option text\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)) {
|
||||
if ($g11 -notmatch $rule) { throw "G11 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.poem-list__copy\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.poem-row__number\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.poem-row__status\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.poem-field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;',
|
||||
'(?s)\.poem-policy > text\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.poem-preview\s*\{[^}]*font-size:\s*23rpx;'
|
||||
)) {
|
||||
if ($g12 -notmatch $rule) { throw "G12 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'G11-G12-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -0,0 +1,13 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$runtime = Get-Content -LiteralPath (Join-Path $root 'tests/g11-g12-settings-poems-runtime-smoke.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'const waitForPageLoad = async (socket, trigger)',
|
||||
"message.method === 'Page.loadEventFired'",
|
||||
"await waitForPageLoad(socket, () => send('Page.reload'))"
|
||||
)) {
|
||||
if ($runtime -notmatch [regex]::Escape($required)) { throw "G11/G12 runtime navigation synchronization missing: $required" }
|
||||
}
|
||||
|
||||
Write-Output 'G11-G12-RUNTIME-NAVIGATION-CONTRACT PASS'
|
||||
@@ -22,7 +22,7 @@ $catalog = Get-Content -LiteralPath (Join-Path $root 'data/page-catalog.js') -Ra
|
||||
foreach ($page in @($g11, $g12)) {
|
||||
if ($page -match '<ModulePage') { throw 'G11-G12 must not retain the generic ModulePage shell' }
|
||||
Assert-Contains $page '<GenealogyPageBackground />' 'G11-G12 must use the accepted shared genealogy background'
|
||||
Assert-Contains $page 'g03-create-flow-panel.png' 'G11-G12 forms must reuse the complete genealogy paper panel'
|
||||
Assert-Contains $page 'g01-empty-panel-frame.png' 'G11-G12 forms must reuse the complete genealogy paper panel'
|
||||
if ($page -match "@/utils/api\.js|\bappApi\b") { throw 'G11-G12 design phase must not connect the API layer' }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ foreach ($required in @(
|
||||
'settings-state--no-permission',
|
||||
'const nameError = ref',
|
||||
'class="settings-feedback"',
|
||||
'g06-search-input-wide.png',
|
||||
'g-form-field-frame.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'const genealogyDraft',
|
||||
'query.genealogyId',
|
||||
@@ -51,7 +51,7 @@ foreach ($required in @(
|
||||
'poem-state--no-permission',
|
||||
'const poemError = ref',
|
||||
'class="poem-feedback"',
|
||||
'g06-search-input-wide.png',
|
||||
'g-form-field-frame.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'a01-scroll-secondary-v3.png',
|
||||
'const poemRows',
|
||||
|
||||
@@ -35,11 +35,31 @@ const waitFor = async (send, expression, message) => {
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
const open = async (send, route, query, selector) => {
|
||||
const waitForPageLoad = async (socket, trigger) => new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
socket.removeEventListener('message', onMessage)
|
||||
reject(new Error('Page reload did not emit Page.loadEventFired'))
|
||||
}, 4000)
|
||||
const onMessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
if (message.method === 'Page.loadEventFired') {
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener('message', onMessage)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
socket.addEventListener('message', onMessage)
|
||||
Promise.resolve().then(trigger).catch((error) => {
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener('message', onMessage)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
const open = async (socket, send, route, query, selector) => {
|
||||
const url = `${origin}/#${route}${query}`
|
||||
await send('Page.navigate', { url })
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `${route} navigation failed`)
|
||||
await send('Page.reload')
|
||||
await waitForPageLoad(socket, () => send('Page.reload'))
|
||||
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `${route}${query} did not render`)
|
||||
}
|
||||
|
||||
@@ -48,11 +68,11 @@ const run = async () => {
|
||||
try {
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
await open(send, '/pages/genealogy/g11-genealogy-settings', '?genealogyId=1001', '.settings-state--form')
|
||||
await open(send, '/pages/genealogy/g11-genealogy-settings', '?state=success&genealogyId=1001', '.settings-state--success')
|
||||
await open(send, '/pages/genealogy/g12-generation-poems', '?genealogyId=1001', '.poem-state--list')
|
||||
await open(send, '/pages/genealogy/g12-generation-poems', '?state=empty&genealogyId=1001', '.poem-state--empty')
|
||||
await open(send, '/pages/genealogy/g12-generation-poems', '?state=edit&genealogyId=1001', '.poem-state--edit')
|
||||
await open(socket, send, '/pages/genealogy/g11-genealogy-settings', '?genealogyId=1001', '.settings-state--form')
|
||||
await open(socket, send, '/pages/genealogy/g11-genealogy-settings', '?state=success&genealogyId=1001', '.settings-state--success')
|
||||
await open(socket, send, '/pages/genealogy/g12-generation-poems', '?genealogyId=1001', '.poem-state--list')
|
||||
await open(socket, send, '/pages/genealogy/g12-generation-poems', '?state=empty&genealogyId=1001', '.poem-state--empty')
|
||||
await open(socket, send, '/pages/genealogy/g12-generation-poems', '?state=edit&genealogyId=1001', '.poem-state--edit')
|
||||
process.stdout.write('G11-G12-SETTINGS-POEMS-RUNTIME-SMOKE PASS\n')
|
||||
} finally {
|
||||
socket.close()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$pagesJsonPath = Join-Path $repoRoot 'pages.json'
|
||||
$componentPath = Join-Path $repoRoot 'components/GenealogyPageBackground.vue'
|
||||
$assetRelativePath = 'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png'
|
||||
$assetPath = Join-Path $repoRoot $assetRelativePath
|
||||
$expectedHash = '9aa9ebdd231eddbc151e238ba340a00f8603f23143117ad8c93009947dcca2e2'
|
||||
|
||||
function Assert-Contract {
|
||||
param(
|
||||
[bool]$Condition,
|
||||
[string]$Message
|
||||
)
|
||||
|
||||
if (-not $Condition) {
|
||||
throw "G shared background contract failed: $Message"
|
||||
}
|
||||
}
|
||||
|
||||
$pagesConfig = Get-Content -Raw -Encoding UTF8 $pagesJsonPath | ConvertFrom-Json
|
||||
$genealogyRoutes = @(
|
||||
$pagesConfig.pages |
|
||||
Where-Object { $_.path -match '^pages/genealogy/g\d{2}-' } |
|
||||
ForEach-Object { $_.path }
|
||||
)
|
||||
|
||||
Assert-Contract ($genealogyRoutes.Count -eq 9) "expected 9 active G routes from pages.json, got $($genealogyRoutes.Count)"
|
||||
Assert-Contract (Test-Path -LiteralPath $componentPath) 'missing components/GenealogyPageBackground.vue'
|
||||
Assert-Contract (Test-Path -LiteralPath $assetPath) "missing $assetRelativePath"
|
||||
|
||||
$componentSource = Get-Content -Raw -Encoding UTF8 $componentPath
|
||||
Assert-Contract ($componentSource -match [regex]::Escape('/static/assets/modules/genealogy/opaque/genealogy-page-background-long.png')) 'component must own the approved long runtime asset path'
|
||||
Assert-Contract ($componentSource -match 'mode="widthFix"') 'shared artwork must use widthFix'
|
||||
Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background\s*\{.*position:\s*fixed;.*inset:\s*0;.*background:\s*#[0-9a-fA-F]{6};.*pointer-events:\s*none;') 'shared background layer must be fixed, full-screen, paper-colored, and non-interactive'
|
||||
Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{.*width:\s*100%;') 'shared artwork must render at full page width'
|
||||
Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{.*bottom:\s*0;') 'shared artwork must stay bottom-aligned on tall viewports'
|
||||
Assert-Contract ($componentSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*top:\s*0;') 'shared artwork must not remain top-aligned'
|
||||
Assert-Contract ($componentSource -match '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:\s*0\.28;') 'shared artwork must use the approved 28% opacity'
|
||||
|
||||
$image = [System.Drawing.Image]::FromFile($assetPath)
|
||||
try {
|
||||
Assert-Contract ($image.Width -eq 1440 -and $image.Height -eq 3600) "expected 1440x3600 runtime background, got $($image.Width)x$($image.Height)"
|
||||
}
|
||||
finally {
|
||||
$image.Dispose()
|
||||
}
|
||||
$actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $assetPath).Hash.ToLowerInvariant()
|
||||
Assert-Contract ($actualHash -eq $expectedHash) "shared runtime asset SHA-256 mismatch: $actualHash"
|
||||
|
||||
foreach ($route in $genealogyRoutes) {
|
||||
$pagePath = Join-Path $repoRoot ($route + '.vue')
|
||||
Assert-Contract (Test-Path -LiteralPath $pagePath) "missing route file $route.vue"
|
||||
|
||||
$pageSource = Get-Content -Raw -Encoding UTF8 $pagePath
|
||||
Assert-Contract ($pageSource -match 'import\s+GenealogyPageBackground\s+from\s+["'']@/components/GenealogyPageBackground\.vue["''];?') "$route must explicitly import GenealogyPageBackground"
|
||||
$componentCount = ([regex]::Matches($pageSource, '<GenealogyPageBackground\s*/>')).Count
|
||||
Assert-Contract ($componentCount -eq 1) "$route must render exactly one GenealogyPageBackground, got $componentCount"
|
||||
Assert-Contract ($pageSource -notmatch 'page-paper\.jpg') "$route still references page-paper.jpg"
|
||||
Assert-Contract ($pageSource -notmatch 'footer-mountain-bamboo\.png') "$route still references footer-mountain-bamboo.png"
|
||||
Assert-Contract ($pageSource -notmatch [regex]::Escape('/static/assets/modules/genealogy/opaque/genealogy-page-background-long.png')) "$route must consume the component instead of the asset directly"
|
||||
Assert-Contract ($pageSource -notmatch '(?s)\.genealogy-page-background__art\s*\{[^}]*opacity:') "$route must not override the shared background opacity"
|
||||
}
|
||||
|
||||
Write-Output 'PASS G genealogy shared background contract'
|
||||
@@ -0,0 +1,86 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$componentsRoot = Join-Path $root 'components'
|
||||
$pagesRoot = Join-Path $root 'pages'
|
||||
|
||||
function Read-Utf8([string]$Path) {
|
||||
return Get-Content -Raw -Encoding UTF8 $Path
|
||||
}
|
||||
|
||||
foreach ($component in @('AppButton.vue', 'AppDialog.vue', 'AppToast.vue', 'AppLoading.vue', 'ModulePageBackground.vue')) {
|
||||
$path = Join-Path $componentsRoot $component
|
||||
if (-not (Test-Path -LiteralPath $path)) { throw "Missing global visual component: $component" }
|
||||
}
|
||||
|
||||
$button = Read-Utf8 (Join-Path $componentsRoot 'AppButton.vue')
|
||||
if ($button -notmatch 'a01-scroll-primary-v3\.png') { throw 'AppButton does not own the approved A-series primary scroll asset.' }
|
||||
if ($button -notmatch 'a01-scroll-secondary-v3\.png') { throw 'AppButton does not own the approved A-series secondary scroll asset.' }
|
||||
|
||||
$dialog = Read-Utf8 (Join-Path $componentsRoot 'AppDialog.vue')
|
||||
if ($dialog -notmatch 'a01-scroll-dialog-v3\.png') { throw 'AppDialog does not own the approved A-series dialog asset.' }
|
||||
|
||||
$toast = Read-Utf8 (Join-Path $componentsRoot 'AppToast.vue')
|
||||
if ($toast -notmatch 'a01-scroll-toast-v3\.png') { throw 'AppToast does not own the approved A-series toast asset.' }
|
||||
|
||||
$header = Read-Utf8 (Join-Path $componentsRoot 'PageHeader.vue')
|
||||
if ($header -notmatch 'chevron-right\.png') { throw 'PageHeader does not use the approved image arrow.' }
|
||||
if ($header -match '\{\{\s*backLabel\s*\}\}') { throw 'PageHeader still renders a text back label.' }
|
||||
if ($header -notmatch 'scaleX\(-1\)') { throw 'PageHeader back arrow is not oriented toward the previous page.' }
|
||||
|
||||
$activeSources = @(
|
||||
Get-ChildItem -LiteralPath $componentsRoot -Recurse -File -Filter '*.vue'
|
||||
Get-ChildItem -LiteralPath $pagesRoot -Recurse -File -Filter '*.vue' | Where-Object { $_.Name -ne 'a06-auth-status.vue' }
|
||||
)
|
||||
foreach ($source in $activeSources) {
|
||||
$content = Read-Utf8 $source.FullName
|
||||
foreach ($forbidden in @(
|
||||
'uni.showToast',
|
||||
'uni.showModal',
|
||||
'uni.showLoading',
|
||||
'uni.showActionSheet',
|
||||
'/static/assets/foundation/opaque/a01-primary-button.png',
|
||||
'/static/assets/foundation/opaque/a01-secondary-button.png'
|
||||
)) {
|
||||
if ($content.Contains($forbidden)) { throw "$($source.FullName) still contains forbidden global UI path: $forbidden" }
|
||||
}
|
||||
}
|
||||
|
||||
$background = Read-Utf8 (Join-Path $componentsRoot 'ModulePageBackground.vue')
|
||||
$moduleAssets = @{
|
||||
tree = 'tree-page-background-long.png'
|
||||
family = 'family-page-background-long.png'
|
||||
records = 'records-page-background-long.png'
|
||||
notification = 'notification-page-background-long.png'
|
||||
profile = 'profile-page-background-long.png'
|
||||
}
|
||||
foreach ($entry in $moduleAssets.GetEnumerator()) {
|
||||
if ($background -notmatch [regex]::Escape($entry.Value)) { throw "ModulePageBackground is missing $($entry.Key) mapping." }
|
||||
$asset = Get-ChildItem -LiteralPath (Join-Path $root 'static/assets/modules') -Recurse -File -Filter $entry.Value
|
||||
if (-not $asset) { throw "Missing module background asset: $($entry.Value)" }
|
||||
$image = [System.Drawing.Image]::FromFile($asset.FullName)
|
||||
try {
|
||||
if ($image.Width -ne 1440 -or $image.Height -ne 3600) {
|
||||
throw "$($entry.Value) must be 1440x3600, got $($image.Width)x$($image.Height)."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$image.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$modulePage = Read-Utf8 (Join-Path $componentsRoot 'ModulePage.vue')
|
||||
foreach ($token in @('<ModulePageBackground', '<AppButton', '<AppToast')) {
|
||||
if ($modulePage -notmatch [regex]::Escape($token)) { throw "ModulePage does not consume $token." }
|
||||
}
|
||||
|
||||
$g01 = Read-Utf8 (Join-Path $pagesRoot 'genealogy/g01-my-genealogies.vue')
|
||||
if ($g01 -notmatch '(?s)\.current-meta\s*\{[^}]*color:\s*#62584c;[^}]*font-weight:\s*500;') { throw 'G01 current genealogy metadata is not using the approved readable style.' }
|
||||
|
||||
$genealogyCard = Read-Utf8 (Join-Path $componentsRoot 'GenealogyCard.vue')
|
||||
foreach ($selector in @('card-meta', 'card-updated', 'card-role')) {
|
||||
if ($genealogyCard -notmatch "(?s)\.$selector\s*\{[^}]*color:\s*#62584c;[^}]*font-weight:\s*500;") { throw "GenealogyCard $selector is not using the approved readable style." }
|
||||
}
|
||||
|
||||
Write-Output 'PASS global heritage visual system contract'
|
||||
@@ -15,8 +15,8 @@ $planning = Get-Content -Raw -Encoding UTF8 (Join-Path $root $planningPath)
|
||||
|
||||
foreach ($required in @(
|
||||
(ConvertFrom-Utf8Base64 'NTIg5p2h5rS75Yqo6Lev55Sx'),
|
||||
(ConvertFrom-Utf8Base64 'NTIg5Liq5b6F5a6h5qC4'),
|
||||
(ConvertFrom-Utf8Base64 'MCDkuKrlt7LpqozmlLY='),
|
||||
(ConvertFrom-Utf8Base64 'NiDkuKrmqKHlnZfln7rlh4Y='),
|
||||
(ConvertFrom-Utf8Base64 'ZG9jcy/lpJzpl7Tmibnph4/mlLbmlZvkuqTmjqVfMjAyNi0wNy0yMC5tZA=='),
|
||||
'product-design:index',
|
||||
'product-design:audit',
|
||||
'imagegen',
|
||||
@@ -44,7 +44,7 @@ foreach ($encodedStale in @(
|
||||
|
||||
$approved = ([regex]::Matches($planning, '\|\s*`\[x\]`[^|]*\|\s*\d+\s*\|')).Count
|
||||
$candidate = ([regex]::Matches($planning, '\|\s*`\[~\]`[^|]*\|\s*\d+\s*\|')).Count
|
||||
if ($approved -ne 0 -or $candidate -ne 52) {
|
||||
if ($approved -ne 6 -or $candidate -ne 46) {
|
||||
throw "planning counts changed: approved=$approved candidate=$candidate"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$m01 = Get-Content -LiteralPath (Join-Path $root 'pages/profile/m01-profile-home.vue') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'/static/assets/foundation/transparent/brand-seal.png',
|
||||
'font-size:\s*25rpx',
|
||||
'font-size:\s*23rpx',
|
||||
'font-size:\s*21rpx'
|
||||
)) {
|
||||
if ($m01 -notmatch $required) { throw "M01 visual contract missing: $required" }
|
||||
}
|
||||
if ($m01.Contains('/static/assets/modules/genealogy/transparent/current-seal-frame.png')) { throw 'M01 must not retain the empty red avatar frame' }
|
||||
|
||||
Write-Output 'M series all-states visual contract passed.'
|
||||
@@ -0,0 +1,35 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$source = Get-Content 'pages/profile/m01-profile-home.vue' -Raw -Encoding UTF8
|
||||
$required = @(
|
||||
'profile-state--ready',
|
||||
'profile-state--error',
|
||||
'm01-profile-summary-card.png',
|
||||
'brand-seal.png',
|
||||
'auth-divider-knot.png',
|
||||
'chevron-right.png',
|
||||
'root-header-hall.png',
|
||||
'a01-scroll-primary-v3.png',
|
||||
'a01-icon-lock-v1.png',
|
||||
'notice.png',
|
||||
'AppButton',
|
||||
'/pages/profile/m02-edit-profile',
|
||||
'/pages/notification/n01-message-center',
|
||||
'/pages/profile/m03-security-settings',
|
||||
'/pages/profile/m06-help-center',
|
||||
'/pages/profile/m10-about-settings'
|
||||
)
|
||||
|
||||
foreach ($token in $required) {
|
||||
if (-not $source.Contains($token)) {
|
||||
throw "M01 missing contract token: $token"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($forbidden in @('m01-profile-notice-card.png', 'm01-profile-menu-frame.png', 'g03-create-flow-panel.png', 'application-status-card.png', 'g06-search-input-wide.png')) {
|
||||
if ($source.Contains($forbidden)) {
|
||||
throw "M01 must not reuse opaque business asset: $forbidden"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output 'M01-MODULE-BASELINE-CONTRACT PASS'
|
||||
@@ -0,0 +1,38 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
function Read-Page([string]$Path) {
|
||||
Get-Content -LiteralPath (Join-Path $root $Path) -Raw -Encoding utf8
|
||||
}
|
||||
|
||||
function Assert-Match {
|
||||
param([string]$Content, [string]$Pattern, [string]$Message)
|
||||
if ($Content -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
$pages = @{
|
||||
T01 = Read-Page 'pages/tree/t01-tree-overview.vue'
|
||||
T03 = Read-Page 'pages/tree/t03-member-profile.vue'
|
||||
T07 = Read-Page 'pages/tree/t07-member-directory.vue'
|
||||
F01 = Read-Page 'pages/family/f01-family-feed.vue'
|
||||
N01 = Read-Page 'pages/notification/n01-message-center.vue'
|
||||
}
|
||||
|
||||
foreach ($entry in $pages.GetEnumerator()) {
|
||||
Assert-Match -Content $entry.Value -Pattern 'import AppLoading from\s*[''"]@/components/AppLoading\.vue[''"]' -Message "$($entry.Key) must import AppLoading"
|
||||
}
|
||||
|
||||
Assert-Match -Content $pages.T01 -Pattern '<AppLoading\s+v-if="treeState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'T01 must render AppLoading before its tree and state cards'
|
||||
Assert-Match -Content $pages.T03 -Pattern '<AppLoading\s+v-if="memberState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'T03 must render AppLoading before detail and error states'
|
||||
Assert-Match -Content $pages.T07 -Pattern '<AppLoading\s+v-if="directoryState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'T07 must render AppLoading before list and state cards'
|
||||
Assert-Match -Content $pages.F01 -Pattern '<AppLoading\s+v-if="feedState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'F01 must render AppLoading before feed content'
|
||||
Assert-Match -Content $pages.N01 -Pattern '<AppLoading\s+v-if="noticeState === ''loading''"[^>]+description="[^"]+"\s*/>' -Message 'N01 must render AppLoading before message content'
|
||||
|
||||
Assert-Match -Content $pages.T01 -Pattern 'query\.state === ''loading''' -Message 'T01 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.T03 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'T03 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.T07 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'T07 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.F01 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'F01 must preserve its loading presentation state'
|
||||
Assert-Match -Content $pages.N01 -Pattern 'query\.state\s*===\s*''loading''\s*\?\s*''loading''' -Message 'N01 must preserve its loading presentation state'
|
||||
|
||||
Write-Output 'MODULE-APP-LOADING-CONTRACT PASS'
|
||||
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
|
||||
function Read-Utf8([string]$Path) {
|
||||
[System.IO.File]::ReadAllText((Join-Path $root $Path), [System.Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
$targets = @(
|
||||
'pages/family/f02-publish-feed.vue',
|
||||
'pages/tree/t03-member-profile.vue',
|
||||
'pages/tree/t08-member-states.vue',
|
||||
'pages/genealogy/g03-create-genealogy.vue',
|
||||
'pages/genealogy/g08-join-application.vue',
|
||||
'pages/genealogy/g09-my-applications.vue',
|
||||
'pages/genealogy/g10-application-review.vue',
|
||||
'pages/genealogy/g11-genealogy-settings.vue',
|
||||
'pages/genealogy/g12-generation-poems.vue'
|
||||
)
|
||||
|
||||
foreach ($target in $targets) {
|
||||
$content = Read-Utf8 $target
|
||||
foreach ($forbidden in @('g03-create-flow-panel.png', 'g06-search-input-wide.png', 'application-status-card.png')) {
|
||||
if ($content.Contains($forbidden)) { throw "$target still uses obsolete opaque cross-module surface: $forbidden" }
|
||||
}
|
||||
}
|
||||
|
||||
$modulePage = Read-Utf8 'components/ModulePage.vue'
|
||||
if ($modulePage.Contains('class="page-intro"')) { throw 'ModulePage must remove repeated numbered intro blocks' }
|
||||
foreach ($required in @('module-content-frame.png', 'module-field-frame.png', '<AppLoading')) {
|
||||
if (-not $modulePage.Contains($required)) { throw "ModulePage baseline promotion missing: $required" }
|
||||
}
|
||||
|
||||
Write-Output 'MODULE-BASELINE-PROMOTION-CONTRACT PASS'
|
||||
@@ -6,14 +6,14 @@ if (-not (Test-Path -LiteralPath $componentPath)) { throw 'Missing shared module
|
||||
$component = Get-Content -LiteralPath $componentPath -Raw -Encoding utf8
|
||||
|
||||
foreach ($anchor in @(
|
||||
"const moduleState = ref('ready')",
|
||||
'module-state--ready',
|
||||
'module-state--empty',
|
||||
'module-state--success',
|
||||
'module-state--error',
|
||||
'g03-create-flow-panel.png',
|
||||
'g06-search-input-wide.png',
|
||||
'application-status-card.png',
|
||||
"const moduleState = ref(['loading', 'empty', 'success', 'error'].includes(query.state) ? query.state : 'ready')",
|
||||
'module-state--${moduleState}',
|
||||
'empty: { eyebrow:',
|
||||
'success: { eyebrow:',
|
||||
'error: { eyebrow:',
|
||||
'module-content-frame.png',
|
||||
'module-field-frame.png',
|
||||
'<AppLoading',
|
||||
'<AppButton',
|
||||
'<AppToast',
|
||||
'<ModulePageBackground',
|
||||
@@ -22,6 +22,15 @@ foreach ($anchor in @(
|
||||
if ($component -notmatch [regex]::Escape($anchor)) { throw "ModulePage is missing high-spec anchor: $anchor" }
|
||||
}
|
||||
|
||||
foreach ($forbidden in @(
|
||||
'class="page-intro"',
|
||||
'g03-create-flow-panel.png',
|
||||
'g06-search-input-wide.png',
|
||||
'application-status-card.png'
|
||||
)) {
|
||||
if ($component -match [regex]::Escape($forbidden)) { throw "ModulePage must not keep obsolete shared surface: $forbidden" }
|
||||
}
|
||||
|
||||
foreach ($className in @('module-panel', 'form-row', 'list-card', 'detail-card', 'timeline-row', 'settings-row', 'status-card')) {
|
||||
foreach ($property in @('border', 'background', 'border-radius', 'box-shadow', 'background-image')) {
|
||||
if ($component -match "(?s)\.$className\s*\{[^}]*\b$property\s*:") {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
const assert = require('assert')
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
const origin = process.argv[2] || 'http://localhost:5173'
|
||||
const sizes = [
|
||||
{ width: 320, height: 568 },
|
||||
{ width: 360, height: 640 },
|
||||
{ width: 360, height: 800 },
|
||||
{ width: 412, height: 915 }
|
||||
]
|
||||
const routes = [
|
||||
['/pages/family/f01-family-feed', '.family-page'],
|
||||
['/pages/family/f02-publish-feed', '.publish-page'],
|
||||
['/pages/family/f04-article-list', '.module-page--list'],
|
||||
['/pages/family/f06-article-editor', '.module-page--form'],
|
||||
['/pages/family/f03-feed-detail', '.module-page--detail'],
|
||||
['/pages/records/r08-growth-journal', '.module-page--timeline'],
|
||||
['/pages/profile/m03-security-settings', '.module-page--settings'],
|
||||
['/pages/family/f10-video-list', '.module-page--status'],
|
||||
['/pages/notification/n01-message-center', '.notice-page'],
|
||||
['/pages/profile/m01-profile-home', '.profile-page']
|
||||
]
|
||||
|
||||
const connect = async () => {
|
||||
const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
|
||||
const page = pages.find((item) => item.type === 'page' && item.url.startsWith(`${origin}/`))
|
||||
if (!page) throw new Error(`Chrome debugging has no ${origin} page`)
|
||||
const socket = new WebSocket(page.webSocketDebuggerUrl)
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.addEventListener('open', resolve, { once: true })
|
||||
socket.addEventListener('error', reject, { once: true })
|
||||
})
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
socket.addEventListener('message', (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
const request = pending.get(message.id)
|
||||
if (!request) return
|
||||
pending.delete(message.id)
|
||||
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
|
||||
})
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
id += 1
|
||||
pending.set(id, { resolve, reject })
|
||||
socket.send(JSON.stringify({ id, method, params }))
|
||||
})
|
||||
return { socket, send }
|
||||
}
|
||||
|
||||
const valueOf = async (send, expression) => (await send('Runtime.evaluate', { expression, returnByValue: true })).result?.value
|
||||
const waitFor = async (send, expression, message) => {
|
||||
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||
if (await valueOf(send, expression)) return
|
||||
await sleep(80)
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const { socket, send } = await connect()
|
||||
let auditId = 0
|
||||
try {
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
for (const size of sizes) {
|
||||
await send('Emulation.setDeviceMetricsOverride', { ...size, deviceScaleFactor: 1, mobile: true, screenWidth: size.width, screenHeight: size.height })
|
||||
for (const [route, selector] of routes) {
|
||||
auditId += 1
|
||||
const url = `${origin}/?responsiveAudit=${auditId}#${route}`
|
||||
await send('Page.navigate', { url })
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)}`, `Navigation failed: ${route} at ${size.width}x${size.height}`)
|
||||
await waitFor(send, `Boolean(document.querySelector(${JSON.stringify(selector)}))`, `State missing: ${route} at ${size.width}x${size.height}`)
|
||||
const metrics = await valueOf(send, `(() => {
|
||||
const root = document.querySelector(${JSON.stringify(selector)})
|
||||
const rect = root.getBoundingClientRect()
|
||||
const visible = Array.from(document.querySelectorAll('.app-button,.list-card,.detail-card,.form-row,.settings-row,.notice-card,.profile-menu'))
|
||||
.filter((node) => node.getClientRects().length)
|
||||
.map((node) => node.getBoundingClientRect())
|
||||
return {
|
||||
documentWidth: document.documentElement.scrollWidth,
|
||||
rootLeft: rect.left,
|
||||
rootRight: rect.right,
|
||||
childOverflow: visible.some((item) => item.left < -1 || item.right > innerWidth + 1)
|
||||
}
|
||||
})()`)
|
||||
assert(metrics.documentWidth <= size.width + 1, `${route} has horizontal document overflow at ${size.width}x${size.height}`)
|
||||
assert(metrics.rootLeft >= -1 && metrics.rootRight <= size.width + 1, `${route} root exceeds viewport at ${size.width}x${size.height}`)
|
||||
assert.strictEqual(metrics.childOverflow, false, `${route} has a visible control outside the viewport at ${size.width}x${size.height}`)
|
||||
}
|
||||
}
|
||||
process.stdout.write('MODULE-SERIES-RESPONSIVE-RUNTIME-SMOKE PASS\n')
|
||||
} finally {
|
||||
try { await send('Emulation.clearDeviceMetricsOverride') } catch (_) {}
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
process.stderr.write(`${error.stack || error.message}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$n01 = Get-Content -LiteralPath (Join-Path $root 'pages/notification/n01-message-center.vue') -Raw -Encoding utf8
|
||||
$capture = Get-Content -LiteralPath (Join-Path $root 'scripts/capture-chrome-page.js') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @('font-size:\s*21rpx','font-size:\s*23rpx','class="notice-review-action" block','<AppButton\s+block\s+:type="noticeState === ''error''')) {
|
||||
if ($n01 -notmatch $required) { throw "N01 visual contract missing: $required" }
|
||||
}
|
||||
foreach ($required in @('prepareNSeriesState','n01-read-first','n01-mark-all')) {
|
||||
if (-not $capture.Contains($required)) { throw "N capture state missing: $required" }
|
||||
}
|
||||
if ($capture -notmatch 'await prepareNSeriesState\(send, action\)') { throw 'Capture runner does not prepare N series states' }
|
||||
|
||||
Write-Output 'N series all-states visual contract passed.'
|
||||
@@ -0,0 +1,32 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$source = Get-Content 'pages/notification/n01-message-center.vue' -Raw -Encoding UTF8
|
||||
$unread = [string]([char]0x672A) + [char]0x8BFB
|
||||
$read = [string]([char]0x5DF2) + [char]0x8BFB
|
||||
$markAllRead = [string]([char]0x5168) + [char]0x90E8 + $read
|
||||
$goToReview = [string]([char]0x524D) + [char]0x5F80 + [char]0x5165 + [char]0x8C31 + [char]0x5BA1 + [char]0x6838
|
||||
$required = @(
|
||||
'n01-notice-card.png',
|
||||
'AppLoading',
|
||||
'AppToast',
|
||||
'notice-state--loading',
|
||||
'notice-state--list',
|
||||
'notice-state--empty',
|
||||
'notice-state--error',
|
||||
$unread,
|
||||
$read,
|
||||
$markAllRead,
|
||||
$goToReview
|
||||
)
|
||||
|
||||
foreach ($token in $required) {
|
||||
if (-not $source.Contains($token)) {
|
||||
throw "N01 missing contract token: $token"
|
||||
}
|
||||
}
|
||||
|
||||
if ($source.Contains('application-status-card.png')) {
|
||||
throw 'N01 must not reuse the genealogy application card'
|
||||
}
|
||||
|
||||
Write-Output 'N01-MODULE-BASELINE-CONTRACT PASS'
|
||||
@@ -0,0 +1,21 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/records/r01-people-list.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match([string]$Pattern, [string]$Message) {
|
||||
if ($page -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
if ($page -match "import\s+ModulePage\s+from|<ModulePage(?:\s|/|>)") { throw 'R01 must own its page instead of delegating to ModulePage.' }
|
||||
if ($page -match 'application-status-card\.png') { throw 'R01 must not reuse the application status card.' }
|
||||
if ($page -match 'people-intro|brand-seal\.png') { throw 'R01 must not repeat a numbered intro block below the page header.' }
|
||||
Assert-Match 'modules/records/transparent/r01-person-name-card\.png' 'R01 must use its dedicated transparent person name card.'
|
||||
Assert-Match 'class="people-search"' 'R01 must expose its search region.'
|
||||
Assert-Match 'people-state--empty' 'R01 must expose an empty review state.'
|
||||
Assert-Match 'people-state--error' 'R01 must expose an error review state.'
|
||||
Assert-Match 'AppButton' 'R01 must use the project button component.'
|
||||
Assert-Match 'AppToast' 'R01 must use the project Toast component.'
|
||||
Assert-Match '/pages/records/r02-person-detail' 'R01 person cards must navigate to R02.'
|
||||
Assert-Match '(?s)\.people-search__action\s*\{[^}]*min-height:\s*44px;' 'R01 search action must preserve a 44 CSS px touch height.'
|
||||
|
||||
Write-Output 'R01-MODULE-BASELINE-CONTRACT PASS'
|
||||
@@ -4,8 +4,10 @@ $root=Split-Path -Parent $PSScriptRoot
|
||||
$files=@('pages/family/f01-family-feed.vue','pages/family/f02-publish-feed.vue','pages/notification/n01-message-center.vue','pages/profile/m01-profile-home.vue')
|
||||
$pages=@{}
|
||||
foreach($file in $files){$content=Get-Content -LiteralPath(Join-Path $root $file)-Raw -Encoding utf8;if($content-match"@/utils/api\.js|\bappApi\b"){ throw "$file must not connect the API layer" };Assert-Contains $content 'ModulePageBackground' "$file must use its module background";$pages[$file]=$content}
|
||||
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','application-status-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
|
||||
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
|
||||
foreach($required in @('notice-state--list','notice-state--empty','notice-state--error','application-status-card.png','AppButton','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
|
||||
foreach($required in @('profile-state--ready','profile-state--error','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
|
||||
foreach($required in @('feed-state--list','feed-state--empty','feed-state--error','f01-family-letter-card.png','/pages/family/f02-publish-feed')){Assert-Contains $pages['pages/family/f01-family-feed.vue'] $required "Missing F01 contract: $required"}
|
||||
foreach($required in @('publish-state--form','publish-state--success','publish-state--error','module-content-frame.png','module-field-frame.png','AppButton','AppToast')){Assert-Contains $pages['pages/family/f02-publish-feed.vue'] $required "Missing F02 contract: $required"}
|
||||
foreach($required in @('notice-state--loading','notice-state--list','notice-state--empty','notice-state--error','n01-notice-card.png','AppButton','AppLoading','AppToast','/pages/genealogy/g10-application-review')){Assert-Contains $pages['pages/notification/n01-message-center.vue'] $required "Missing N01 contract: $required"}
|
||||
if($pages['pages/notification/n01-message-center.vue']-match'application-status-card\.png'){throw 'N01 must not reuse the genealogy application card'}
|
||||
foreach($required in @('profile-state--ready','profile-state--error','m01-profile-summary-card.png','brand-seal.png','auth-divider-knot.png','chevron-right.png','AppButton','/pages/profile/m02-edit-profile')){Assert-Contains $pages['pages/profile/m01-profile-home.vue'] $required "Missing M01 contract: $required"}
|
||||
foreach($forbidden in @('m01-profile-notice-card.png','m01-profile-menu-frame.png','g03-create-flow-panel.png','g06-search-input-wide.png','application-status-card.png')){if($pages['pages/profile/m01-profile-home.vue']-match[regex]::Escape($forbidden)){throw "M01 must not reuse repeated or opaque business asset: $forbidden"}}
|
||||
Write-Output 'ROOT-PAGES-VISUAL-CONTRACT PASS'
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t01-tree-overview.vue') -Raw -Encoding utf8
|
||||
|
||||
foreach ($required in @(
|
||||
'a01-primary-button-v2.png',
|
||||
'a01-secondary-button-v2.png',
|
||||
't01-member-node-standard.png',
|
||||
't01-member-node-selected.png',
|
||||
't01-state-panel.png',
|
||||
't01-member-drawer.png',
|
||||
'lineage-connector--root',
|
||||
'lineage-pan-cue'
|
||||
)) {
|
||||
if ($page -notmatch [regex]::Escape($required)) { throw "T01 member action asset missing: $required" }
|
||||
}
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.tree-toolbar__title text:last-child\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.tree-toolbar__actions\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.generation-band\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.node-relation\s*\{[^}]*font-size:\s*21rpx;',
|
||||
'(?s)\.node-years\s*\{[^}]*font-size:\s*20rpx;',
|
||||
'(?s)\.tree-state-card__copy\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.sheet-meta\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.sheet-action text\s*\{[^}]*font-family:\s*''Microsoft YaHei''[^}]*font-size:\s*22rpx;'
|
||||
)) {
|
||||
if ($page -notmatch $rule) { throw "T01 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'T01-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -33,9 +33,14 @@ foreach ($required in @(
|
||||
'tree-state--empty',
|
||||
'tree-state--error',
|
||||
'tree-canvas--state',
|
||||
'g03-create-flow-panel.png',
|
||||
'g06-search-input-wide.png',
|
||||
'application-status-card.png',
|
||||
'lineage-connector',
|
||||
'generation-band--twelve',
|
||||
'generation-band--thirteen',
|
||||
'generation-band--fourteen',
|
||||
't01-member-node-standard.png',
|
||||
't01-member-node-selected.png',
|
||||
't01-state-panel.png',
|
||||
't01-member-drawer.png',
|
||||
'/pages/tree/t03-member-profile',
|
||||
'/pages/tree/t04-add-relative',
|
||||
'/pages/tree/t06-edit-relationship',
|
||||
@@ -43,6 +48,14 @@ foreach ($required in @(
|
||||
'query.genealogyId'
|
||||
)) { Assert-Contains $page $required "Missing T01 contract: $required" }
|
||||
|
||||
foreach ($forbidden in @(
|
||||
'g03-create-flow-panel.png',
|
||||
'g06-search-input-wide.png',
|
||||
'application-status-card.png'
|
||||
)) {
|
||||
if ($page -match [regex]::Escape($forbidden)) { throw "T01 must not keep opaque surface asset: $forbidden" }
|
||||
}
|
||||
|
||||
foreach ($className in @('tree-canvas', 'member-node', 'member-sheet', 'tree-state-card')) {
|
||||
Assert-NoCssSurface $page $className
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$profile = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t03-member-profile.vue') -Raw -Encoding utf8
|
||||
$form = Get-Content -LiteralPath (Join-Path $root 'components/tree/TreeMemberForm.vue') -Raw -Encoding utf8
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.member-heading > view:last-child text:last-child\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.member-section-title\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.member-info-row text\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.member-relatives\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.member-error > text:nth-child\(2\)\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)) {
|
||||
if ($profile -notmatch $rule) { throw "T03 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.member-form__copy\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.member-field > text\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.member-field input\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.member-form__note\s*\{[^}]*font-size:\s*22rpx;'
|
||||
)) {
|
||||
if ($form -notmatch $rule) { throw "T04-T06 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'T03-T06-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
@@ -27,7 +27,7 @@ foreach ($path in @('pages/tree/t03-member-profile.vue', 'pages/tree/t07-member-
|
||||
}
|
||||
|
||||
$t03 = $pages['pages/tree/t03-member-profile.vue']
|
||||
foreach ($required in @('member-state--detail', 'member-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', '/pages/tree/t05-edit-member', 'query.personId')) {
|
||||
foreach ($required in @('member-state--detail', 'member-state--error', 't01-state-panel.png', 't07-search-input-frame.png', '/pages/tree/t05-edit-member', 'query.personId')) {
|
||||
Assert-Contains $t03 $required "Missing T03 contract: $required"
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ if ($form -match "@/utils/api\.js|\bappApi\b") { throw 'TreeMemberForm must not
|
||||
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
|
||||
if ($form -match [regex]::Escape($nativeUi)) { throw "TreeMemberForm must use project-owned feedback instead of $nativeUi" }
|
||||
}
|
||||
foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 'g03-create-flow-panel.png', 'g06-search-input-wide.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'AppDialog', 'ModulePageBackground')) {
|
||||
foreach ($required in @('form-state--form', 'form-state--success', 'form-state--conflict', 'form-state--error', 't01-state-panel.png', 't07-search-input-frame.png', 'a01-scroll-primary-v3.png', 'a01-scroll-secondary-v3.png', 'AppDialog', 'ModulePageBackground')) {
|
||||
Assert-Contains $form $required "Missing shared form contract: $required"
|
||||
}
|
||||
foreach ($route in @('t04-add-relative.vue', 't05-edit-member.vue', 't06-edit-relationship.vue')) {
|
||||
@@ -46,12 +46,12 @@ foreach ($route in @('t04-add-relative.vue', 't05-edit-member.vue', 't06-edit-re
|
||||
}
|
||||
|
||||
$t07 = $pages['pages/tree/t07-member-directory.vue']
|
||||
foreach ($required in @('directory-state--list', 'directory-state--empty', 'directory-state--error', 'application-status-card.png', 'g06-search-input-wide.png', '/pages/tree/t03-member-profile')) {
|
||||
foreach ($required in @('directory-state--list', 'directory-state--empty', 'directory-state--error', 'list-slip-frame.png', 'directory-card__status', '/pages/tree/t03-member-profile')) {
|
||||
Assert-Contains $t07 $required "Missing T07 contract: $required"
|
||||
}
|
||||
|
||||
$t08 = $pages['pages/tree/t08-member-states.vue']
|
||||
foreach ($required in @('member-status--privacy', 'member-status--deceased', 'member-status--forbidden', 'application-status-card.png', 'query.state')) {
|
||||
foreach ($required in @('member-status--privacy', 'member-status--deceased', 'member-status--forbidden', 't01-state-panel.png', 'query.state')) {
|
||||
Assert-Contains $t08 $required "Missing T08 contract: $required"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
|
||||
|
||||
function Assert-Match([string]$Pattern, [string]$Message) {
|
||||
if ($page -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-Match 'class="directory-context"' 'T07 must show the current genealogy context.'
|
||||
Assert-Match '\u6c64\u6c0f\u5bb6\u8c31' 'T07 baseline must identify the current genealogy in the H5 mock state.'
|
||||
Assert-Match '''directory-state--loading'': directoryState === ''loading''' 'T07 loading state must have an explicit root class.'
|
||||
Assert-Match 'v-if="directoryState === ''list'' \|\| directoryState === ''empty''"' 'T07 search must remain available for list and empty states only.'
|
||||
Assert-Match 'class="directory-search__action"[^>]*role="button"[^>]*aria-label="\u67e5\u627e\u6210\u5458"' 'T07 search action must expose button semantics and an accessible label.'
|
||||
Assert-Match '<AppButton\s+v-if="directoryState === ''error''"[^>]+type="secondary"[^>]+label="\u91cd\u65b0\u67e5\u770b"[^>]+@click="retryDirectory"' 'T07 error state must provide a custom recovery action.'
|
||||
Assert-Match 'const retryDirectory = \(\) => \{ directoryState\.value = ''list'' \}' 'T07 retry action must return to the list state.'
|
||||
Assert-Match '(?s)\.directory-page\s*\{[^}]*overflow-x:\s*hidden;[^}]*overflow-y:\s*auto;' 'T07 must allow natural vertical scrolling without horizontal overflow.'
|
||||
Assert-Match '(?s)\.directory-search\s*\{[^}]*height:\s*44px;' 'T07 search field must preserve a 44 CSS px touch height at every viewport.'
|
||||
Assert-Match '(?s)\.directory-search__action\s*\{[^}]*min-width:\s*88rpx;[^}]*min-height:\s*44px;' 'T07 search action must preserve a 44 CSS px touch target.'
|
||||
Assert-Match 'modules/tree/transparent/t07-search-input-frame\.png' 'T07 search must use its dedicated transparent input frame.'
|
||||
Assert-Match 'modules/genealogy/transparent/list-slip-frame\.png' 'T07 member cards must use the approved transparent list frame.'
|
||||
if ($page -match 'g06-search-input-wide\.png|application-status-card\.png') {
|
||||
throw 'T07 must not reuse opaque search or application-status skins.'
|
||||
}
|
||||
if ($page -match 'row-seal-frame\.png|directory-card__seal') {
|
||||
throw 'T07 member cards must not repeat generation information in decorative seals.'
|
||||
}
|
||||
Assert-Match 'class="directory-card__status"' 'T07 member identity or profile state must use a dedicated status label.'
|
||||
|
||||
foreach ($state in @('loading', 'list', 'empty', 'error')) {
|
||||
Assert-Match $state "T07 must preserve the $state state."
|
||||
}
|
||||
|
||||
Write-Output 'T07-MODULE-BASELINE-CONTRACT PASS'
|
||||
@@ -0,0 +1,181 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const origin = 'http://localhost:5173'
|
||||
const outputDirectory = path.resolve('docs/design/screens/runtime/2026-07-19/t07-baseline')
|
||||
const sizes = [
|
||||
{ width: 320, height: 568 },
|
||||
{ width: 360, height: 640 },
|
||||
{ width: 360, height: 800 },
|
||||
{ width: 412, height: 915 }
|
||||
]
|
||||
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
|
||||
|
||||
const connect = async () => {
|
||||
const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
|
||||
const projectPages = pages.filter((page) => page.type === 'page' && page.url.startsWith(origin))
|
||||
if (projectPages.length !== 1) throw new Error(`Expected one project page, found ${projectPages.length}`)
|
||||
const socket = new WebSocket(projectPages[0].webSocketDebuggerUrl)
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.addEventListener('open', resolve, { once: true })
|
||||
socket.addEventListener('error', reject, { once: true })
|
||||
})
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
socket.addEventListener('message', (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
const request = pending.get(message.id)
|
||||
if (!request) return
|
||||
pending.delete(message.id)
|
||||
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
|
||||
})
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
id += 1
|
||||
pending.set(id, { resolve, reject })
|
||||
socket.send(JSON.stringify({ id, method, params }))
|
||||
})
|
||||
return { projectPageCount: projectPages.length, socket, send }
|
||||
}
|
||||
|
||||
const valueOf = async (send, expression) => {
|
||||
const result = await send('Runtime.evaluate', { expression, returnByValue: true })
|
||||
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime evaluation failed')
|
||||
return result.result?.value
|
||||
}
|
||||
|
||||
const waitFor = async (send, expression, message) => {
|
||||
for (let attempt = 0; attempt < 60; attempt += 1) {
|
||||
try {
|
||||
if (await valueOf(send, expression)) return
|
||||
} catch (error) {
|
||||
if (!String(error.message).includes('Inspected target navigated or closed')) throw error
|
||||
}
|
||||
await sleep(100)
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const setSize = (send, size) => send('Emulation.setDeviceMetricsOverride', {
|
||||
...size,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: true,
|
||||
screenWidth: size.width,
|
||||
screenHeight: size.height
|
||||
})
|
||||
|
||||
let navigationId = 0
|
||||
const openState = async (send, query = '') => {
|
||||
navigationId += 1
|
||||
const url = `${origin}/?t07Baseline=${navigationId}#/pages/tree/t07-member-directory${query}`
|
||||
await send('Page.navigate', { url })
|
||||
await waitFor(send, `location.href === ${JSON.stringify(url)} && Boolean(document.querySelector('.directory-page'))`, `T07 did not render ${query || 'list'}`)
|
||||
await sleep(250)
|
||||
}
|
||||
|
||||
const capture = async (send, filename) => {
|
||||
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false })
|
||||
fs.mkdirSync(outputDirectory, { recursive: true })
|
||||
fs.writeFileSync(path.join(outputDirectory, filename), Buffer.from(screenshot.data, 'base64'))
|
||||
}
|
||||
|
||||
const metrics = (send) => valueOf(send, `(() => {
|
||||
const rect = (selector) => {
|
||||
const node = document.querySelector(selector)
|
||||
if (!node) return null
|
||||
const box = node.getBoundingClientRect()
|
||||
return { top: box.top, right: box.right, bottom: box.bottom, left: box.left, width: box.width, height: box.height }
|
||||
}
|
||||
return {
|
||||
viewport: { width: innerWidth, height: innerHeight },
|
||||
context: rect('.directory-context'),
|
||||
search: rect('.directory-search'),
|
||||
firstCard: rect('.directory-card'),
|
||||
lastCard: rect('.directory-card:last-child'),
|
||||
cardCount: document.querySelectorAll('.directory-card').length,
|
||||
state: document.querySelector('.directory-state--loading') ? 'loading' : document.querySelector('.directory-state--empty') ? 'empty' : document.querySelector('.directory-state--error') ? 'error' : 'list',
|
||||
horizontalOverflow: document.documentElement.scrollWidth > innerWidth || document.body.scrollWidth > innerWidth,
|
||||
documentScrollHeight: document.documentElement.scrollHeight
|
||||
}
|
||||
})()`)
|
||||
|
||||
const assert = (condition, message) => { if (!condition) throw new Error(message) }
|
||||
|
||||
const run = async () => {
|
||||
const { projectPageCount, socket, send } = await connect()
|
||||
const runtimeErrors = []
|
||||
const resourceErrors = []
|
||||
socket.addEventListener('message', (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
if (message.method === 'Runtime.exceptionThrown') runtimeErrors.push(message.params.exceptionDetails?.text || 'runtime exception')
|
||||
if (message.method === 'Network.responseReceived' && message.params.response.status >= 400) resourceErrors.push(`${message.params.response.status} ${message.params.response.url}`)
|
||||
})
|
||||
|
||||
try {
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
await send('Network.enable')
|
||||
const responsive = []
|
||||
for (const size of sizes) {
|
||||
await setSize(send, size)
|
||||
await openState(send, '?genealogyId=1001')
|
||||
const current = await metrics(send)
|
||||
assert(!current.horizontalOverflow, `${size.width}x${size.height}: horizontal overflow`)
|
||||
assert(current.context && current.search && current.firstCard, `${size.width}x${size.height}: missing baseline content`)
|
||||
assert(current.cardCount === 3, `${size.width}x${size.height}: expected three members`)
|
||||
await valueOf(send, 'scrollTo(0, document.documentElement.scrollHeight)')
|
||||
await sleep(50)
|
||||
const lastBottom = await valueOf(send, "document.querySelector('.directory-card:last-child').getBoundingClientRect().bottom")
|
||||
assert(lastBottom <= size.height + 1, `${size.width}x${size.height}: last member is not reachable`)
|
||||
await valueOf(send, 'scrollTo(0, 0)')
|
||||
responsive.push(current)
|
||||
await capture(send, `responsive-${size.width}x${size.height}.png`)
|
||||
}
|
||||
|
||||
await setSize(send, { width: 412, height: 915 })
|
||||
await openState(send, '?state=loading&genealogyId=1001')
|
||||
assert((await metrics(send)).state === 'loading', 'Loading state did not render')
|
||||
await capture(send, '02-loading-412x915.png')
|
||||
|
||||
await openState(send, '?state=empty&genealogyId=1001')
|
||||
assert((await metrics(send)).state === 'empty', 'Empty state did not render')
|
||||
await capture(send, '03-empty-412x915.png')
|
||||
|
||||
await openState(send, '?state=error&genealogyId=1001')
|
||||
assert((await metrics(send)).state === 'error', 'Error state did not render')
|
||||
assert(!await valueOf(send, "Boolean(document.querySelector('.directory-search'))"), 'Error state must not expose search')
|
||||
await capture(send, '04-error-412x915.png')
|
||||
await valueOf(send, "document.querySelector('.directory-content .app-button').click()")
|
||||
await waitFor(send, "document.querySelectorAll('.directory-card').length === 3 && Boolean(document.querySelector('.directory-search'))", 'Error retry did not restore the list')
|
||||
|
||||
const inputSearch = async (value) => {
|
||||
await valueOf(send, `(() => {
|
||||
const input = document.querySelector('.directory-search input')
|
||||
input.value = ${JSON.stringify(value)}
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
return true
|
||||
})()`)
|
||||
await sleep(50)
|
||||
await valueOf(send, "document.querySelector('.directory-search__action').click()")
|
||||
}
|
||||
await inputSearch('不存在')
|
||||
await waitFor(send, "Boolean(document.querySelector('.directory-state--empty'))", 'No-result search did not render the empty state')
|
||||
await inputSearch('')
|
||||
await waitFor(send, "document.querySelectorAll('.directory-card').length === 3", 'Cleared search did not restore all members')
|
||||
|
||||
await openState(send, '?genealogyId=1001')
|
||||
await capture(send, '01-list-412x915.png')
|
||||
const final = await metrics(send)
|
||||
assert(final.viewport.width === 412 && final.viewport.height === 915 && final.state === 'list', 'Final approval state is invalid')
|
||||
assert(runtimeErrors.length === 0, `Runtime errors: ${runtimeErrors.join(' | ')}`)
|
||||
assert(resourceErrors.length === 0, `Resource errors: ${resourceErrors.join(' | ')}`)
|
||||
process.stdout.write(`${JSON.stringify({ projectPageCount, responsive, final, runtimeErrors, resourceErrors }, null, 2)}\n`)
|
||||
process.stdout.write('PASS T07 module baseline runtime smoke\n')
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
process.stderr.write(`${error.stack || error.message}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$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
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.directory-search input\s*\{[^}]*font-size:\s*26rpx;',
|
||||
'(?s)\.directory-summary text:last-child\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.directory-card__meta\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.directory-card__status\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.directory-state-card text:last-child\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)) {
|
||||
if ($t07 -notmatch $rule) { throw "T07 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
foreach ($rule in @(
|
||||
'(?s)\.status-tab text\s*\{[^}]*font-size:\s*23rpx;',
|
||||
'(?s)\.status-card__copy text:first-child\s*\{[^}]*font-size:\s*22rpx;',
|
||||
'(?s)\.status-card__copy text:nth-child\(2\)\s*\{[^}]*font-size:\s*32rpx;',
|
||||
'(?s)\.status-card__copy text:last-child\s*\{[^}]*font-size:\s*24rpx;',
|
||||
'(?s)\.status-guidance text\s*\{[^}]*font-size:\s*24rpx;'
|
||||
)) {
|
||||
if ($t08 -notmatch $rule) { throw "T08 readable visual rule missing: $rule" }
|
||||
}
|
||||
|
||||
Write-Output 'T07-T08-ALL-STATES-VISUAL-CONTRACT PASS'
|
||||
Reference in New Issue
Block a user