Review changes batch 5 of 6

This commit is contained in:
2026-07-20 06:52:26 +08:00
parent 0d0645a112
commit db97d3da27
41 changed files with 6672 additions and 1124 deletions
+256
View File
@@ -0,0 +1,256 @@
const fs = require('fs')
const path = require('path')
const origin = 'http://localhost:5173'
const outputDirectory = path.resolve('docs/design/screens/runtime/2026-07-19/g01-approval')
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)
if (message.error) request.reject(new Error(message.error.message))
else 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 < 50; attempt += 1) {
if (await valueOf(send, expression)) return
await sleep(100)
}
throw new Error(message)
}
const setSize = async (send, width, height) => {
await send('Emulation.setDeviceMetricsOverride', {
width,
height,
deviceScaleFactor: 1,
mobile: true,
screenWidth: width,
screenHeight: height
})
await sleep(150)
}
const openSwitcher = async (send) => {
if (!await valueOf(send, "Boolean(document.querySelector('.genealogy-switcher'))")) {
const clicked = await valueOf(send, `(() => {
const trigger = document.querySelector('.current-slip')
if (!trigger) return false
trigger.click()
return true
})()`)
if (!clicked) throw new Error('Could not find the switch genealogy trigger')
}
await waitFor(send, "Boolean(document.querySelector('.genealogy-switcher'))", 'Switcher did not open')
}
const clearClones = (send) => valueOf(send, `(() => {
document.querySelectorAll('[data-cdp-clone="1"]').forEach((node) => node.remove())
return true
})()`)
const addClonesToTotal = (send, total) => valueOf(send, `(() => {
document.querySelectorAll('[data-cdp-clone="1"]').forEach((node) => node.remove())
const list = document.querySelector('.genealogy-switcher__list')
const originals = Array.from(list?.querySelectorAll('.switcher-item') || [])
if (!list || originals.length !== 2) return false
for (let index = originals.length; index < ${total}; index += 1) {
const clone = originals[index % originals.length].cloneNode(true)
clone.dataset.cdpClone = '1'
clone.querySelector('.switcher-item__name').textContent = '压力测试家谱 ' + (index + 1)
clone.querySelector('.switcher-item__state').textContent = '选择'
clone.classList.remove('switcher-item--active')
list.appendChild(clone)
}
return true
})()`)
const getMetrics = (send) => valueOf(send, `(() => {
const dialog = document.querySelector('.genealogy-switcher')
const content = document.querySelector('.genealogy-switcher__content')
const title = document.querySelector('.dialog-title')
const close = document.querySelector('.genealogy-switcher__close')
const list = document.querySelector('.genealogy-switcher__list')
const items = Array.from(document.querySelectorAll('.genealogy-switcher__list .switcher-item'))
const rect = (node) => node ? { top: node.getBoundingClientRect().top, right: node.getBoundingClientRect().right, bottom: node.getBoundingClientRect().bottom, left: node.getBoundingClientRect().left, width: node.getBoundingClientRect().width, height: node.getBoundingClientRect().height } : null
const last = items.at(-1)
return {
viewport: { width: innerWidth, height: innerHeight },
dialog: rect(dialog),
content: rect(content),
title: rect(title),
close: rect(close),
list: rect(list),
itemCount: items.length,
cloneCount: document.querySelectorAll('[data-cdp-clone="1"]').length,
listClientHeight: list?.clientHeight || 0,
listScrollHeight: list?.scrollHeight || 0,
listScrollTop: list?.scrollTop || 0,
blankBelowLastItem: list && last ? list.getBoundingClientRect().bottom - last.getBoundingClientRect().bottom : null,
horizontalOverflow: document.documentElement.scrollWidth > innerWidth || document.body.scrollWidth > innerWidth,
addDialogVisible: Boolean(document.querySelector('.add-genealogy-sheet')),
switcherVisible: Boolean(dialog),
borderImageSource: dialog ? getComputedStyle(dialog).borderImageSource : '',
borderImageSlice: dialog ? getComputedStyle(dialog).borderImageSlice : ''
}
})()`)
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 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')
await setSize(send, 412, 915)
await send('Page.reload')
await waitFor(send, "Boolean(document.querySelector('.current-slip'))", 'G01 did not render after reload')
await openSwitcher(send)
const sizeResults = []
for (const size of sizes) {
await setSize(send, size.width, size.height)
await clearClones(send)
await openSwitcher(send)
const metrics = await getMetrics(send)
const rpx = size.width / 750
assert(metrics.itemCount === 2, `${size.width}x${size.height}: expected two items`)
const expectedDialogHeight = (600 * rpx) + 2
assert(Math.abs(metrics.dialog.height - expectedDialogHeight) < 1, `${size.width}x${size.height}: dialog height ${metrics.dialog.height}px is not the expected ${expectedDialogHeight}px including its 1px border`)
assert(metrics.title.top > metrics.dialog.top + (78 * rpx), `${size.width}x${size.height}: title overlaps top decoration`)
assert(metrics.close.top >= metrics.dialog.top && metrics.close.right <= metrics.dialog.right + 1, `${size.width}x${size.height}: close control is clipped`)
assert(metrics.listScrollHeight <= metrics.listClientHeight + 1, `${size.width}x${size.height}: two-item list unexpectedly scrolls`)
assert(metrics.blankBelowLastItem <= (72 * rpx) + 2, `${size.width}x${size.height}: too much blank space below last item`)
assert(!metrics.horizontalOverflow, `${size.width}x${size.height}: horizontal overflow`)
sizeResults.push(metrics)
await capture(send, `06-switch-dialog-stretchable-${size.width}x${size.height}.png`)
}
await setSize(send, 412, 915)
assert(await addClonesToTotal(send, 6), 'Could not prepare six-item stress state')
await sleep(100)
const sixItems = await getMetrics(send)
assert(sixItems.itemCount === 6, 'Six-item state did not contain six items')
assert(sixItems.dialog.height > sizeResults.at(-1).dialog.height + 1, 'Six-item dialog did not grow')
assert(sixItems.dialog.height < 915 - ((120 * 412) / 750) - 1, 'Six-item dialog reached the maximum height too early')
assert(sixItems.listScrollHeight <= sixItems.listClientHeight + 1, 'Six-item list unexpectedly scrolls')
await capture(send, '06-switch-dialog-stretchable-six-items-412x915.png')
assert(await addClonesToTotal(send, 12), 'Could not prepare twelve-item stress state')
await sleep(100)
const twelveItemsTop = await getMetrics(send)
const expectedMaximumHeight = 915 - ((120 * 412) / 750)
assert(Math.abs(twelveItemsTop.dialog.height - expectedMaximumHeight) < 3, `Twelve-item dialog height ${twelveItemsTop.dialog.height}px did not stop at the expected ${expectedMaximumHeight}px safe maximum: ${JSON.stringify(twelveItemsTop)}`)
assert(twelveItemsTop.listScrollHeight > twelveItemsTop.listClientHeight + 1, 'Twelve-item list does not scroll')
assert(twelveItemsTop.listScrollTop === 0, 'Twelve-item list did not start at the top')
await valueOf(send, `(() => {
const list = document.querySelector('.genealogy-switcher__list')
list.scrollTop = list.scrollHeight
return true
})()`)
await sleep(50)
const twelveItemsBottom = await getMetrics(send)
assert(twelveItemsBottom.listScrollTop > 0, 'Twelve-item list did not scroll to the bottom')
await clearClones(send)
await valueOf(send, "document.querySelector('.genealogy-switcher__content').click()")
assert(await valueOf(send, "Boolean(document.querySelector('.genealogy-switcher'))"), 'Inner click closed the switcher')
await valueOf(send, "document.querySelector('.genealogy-switcher__close').click()")
assert(!await valueOf(send, "Boolean(document.querySelector('.genealogy-switcher'))"), 'Close icon did not close the switcher')
await openSwitcher(send)
await valueOf(send, "document.querySelector('.genealogy-switcher-layer').click()")
assert(!await valueOf(send, "Boolean(document.querySelector('.genealogy-switcher'))"), 'Mask did not close the switcher')
await openSwitcher(send)
await valueOf(send, "document.querySelectorAll('.switcher-item')[1].click()")
await waitFor(send, "!document.querySelector('.genealogy-switcher')", 'Selecting the second genealogy did not close the switcher')
assert((await valueOf(send, "document.querySelector('.current-slip')?.textContent"))?.includes('山东'), 'Selecting the second genealogy did not update the current genealogy')
await openSwitcher(send)
await valueOf(send, "document.querySelectorAll('.switcher-item')[0].click()")
await waitFor(send, "!document.querySelector('.genealogy-switcher')", 'Restoring the first genealogy did not close the switcher')
await openSwitcher(send)
await clearClones(send)
const final = await getMetrics(send)
assert(final.viewport.width === 412 && final.viewport.height === 915, 'Final viewport is not 412x915')
assert(final.itemCount === 2 && final.cloneCount === 0, 'Final state contains temporary items')
assert(final.switcherVisible && !final.addDialogVisible, 'Final state does not show only the switcher')
assert(!final.horizontalOverflow, 'Final state has horizontal overflow')
assert(runtimeErrors.length === 0, `Runtime exceptions: ${runtimeErrors.join(' | ')}`)
assert(resourceErrors.length === 0, `Resource errors: ${resourceErrors.join(' | ')}`)
process.stdout.write(`${JSON.stringify({ projectPageCount, sizeResults, sixItems, twelveItemsTop, twelveItemsBottom, final, runtimeErrors, resourceErrors }, null, 2)}\n`)
process.stdout.write('PASS G-01 switch dialog runtime smoke\n')
} finally {
socket.close()
}
}
run().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`)
process.exit(1)
})
+129 -3
View File
@@ -25,6 +25,10 @@ $requiredAssets = @(
'static/assets/foundation/transparent/tab-profile-active.png',
'static/assets/modules/genealogy/transparent/current-slip-frame.png',
'static/assets/modules/genealogy/transparent/list-slip-frame.png',
'static/assets/modules/genealogy/transparent/g01-dialog-close.png',
'static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png',
'static/assets/modules/auth/transparent/a01-paper-transition-v1.png',
'static/assets/foundation/opaque/page-paper.jpg',
'static/assets/foundation/opaque/root-header-cinnabar.jpg',
'static/assets/modules/genealogy/opaque/genealogy-page-background-long.png',
'static/assets/modules/genealogy/transparent/section-divider.png'
@@ -65,6 +69,113 @@ foreach ($state in @('hasError', 'applicationRecords', 'addDialogVisible', 'swit
foreach ($required in @('class="add-dialog-layer"', 'class="genealogy-switcher-layer"', 'application-section')) {
if ($page -notmatch [regex]::Escape($required)) { throw "G-01 is missing current acceptance structure: $required" }
}
$closeCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('5YWz6Zet'))
$cancelCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('5Y+W5raI'))
$searchCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('5pCc57Si5a626LCx'))
$inviteCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('6YKA6K+356CB5Yqg5YWl'))
$continueCreateCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('57un57ut5Yib5bu65a626LCx'))
$legacyCreateCopy = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('56Gu6K6k5rKh5pyJ546w5pyJ5a626LCx77yM57un57ut5Yib5bu6'))
$addMarkup = [regex]::Match($page, '(?s)<view v-if="addDialogVisible".*?<view v-if="switcherVisible"').Value
if (-not $addMarkup) { throw 'G-01 add sheet markup could not be isolated.' }
foreach ($token in @(
'class="add-dialog__body"',
'class="add-dialog__close-icon"',
'class="add-dialog__actions"',
'g01-dialog-close.png',
"aria-label=`"$closeCopy`""
)) {
if ($addMarkup -notmatch [regex]::Escape($token)) { throw "G-01 paper sheet is missing $token" }
}
if ($addMarkup -notmatch '(?s)<view class="add-dialog__body">\s*<view class="add-dialog__heading">.*?class="add-dialog__close".*?</view>\s*</view>\s*<view class="add-dialog__actions">') {
throw 'G-01 add sheet must keep the close control with the centered heading and actions body.'
}
if ($addMarkup -match 'add-dialog__paper|add-dialog__edge|page-paper\.jpg|a01-paper-transition-v1\.png') {
throw 'G-01 add sheet still assembles its background from separate paper and edge layers.'
}
if ($addMarkup -match 'a01-scroll-dialog-v3\.png') { throw 'G-01 add sheet still uses the rejected complete dialog frame.' }
foreach ($copy in @($closeCopy, $cancelCopy)) {
if ($addMarkup -match [regex]::Escape(">$copy</view>")) { throw 'G-01 add sheet still renders a text close control.' }
}
foreach ($copy in @($searchCopy, $inviteCopy, $continueCreateCopy)) {
$token = "label=`"$copy`""
if ($addMarkup -notmatch [regex]::Escape($token)) { throw "G-01 add sheet no longer reuses AppButton action $token" }
}
if ($addMarkup -match [regex]::Escape("label=`"$legacyCreateCopy`"")) {
throw 'G-01 add sheet still puts guidance copy inside the create button.'
}
if ($page -notmatch '(?s)\.add-dialog-layer\s*\{[^}]*align-items:\s*flex-end;[^}]*background:\s*rgba\(34,\s*20,\s*12,\s*0\.68\);') {
throw 'G-01 add sheet is not a bottom-aligned layer with the approved mask.'
}
if ($page -notmatch '(?s)\.add-dialog__close\s*\{[^}]*width:\s*80rpx;[^}]*height:\s*80rpx;') {
throw 'G-01 add sheet close control does not reserve the approved hit area.'
}
if ($page -notmatch '(?s)\.add-dialog\s*\{[^}]*min-height:\s*780rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*80rpx\);') {
throw 'G-01 add sheet does not reach the approved arrow-aligned minimum height.'
}
if ($page -notmatch '(?s)\.add-dialog__content\s*\{[^}]*min-height:\s*780rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*80rpx\);[^}]*padding:\s*96rpx\s+52rpx\s+calc\(96rpx\s*\+\s*env\(safe-area-inset-bottom\)\);') {
throw 'G-01 add sheet does not reserve the approved symmetric centering area.'
}
if ($page -notmatch '(?s)\.add-dialog__body\s*\{[^}]*margin:\s*auto\s+0;') {
throw 'G-01 add sheet body does not center safely with collapsible auto margins.'
}
if ($page -notmatch '(?s)\.add-dialog__heading\s*\{[^}]*position:\s*relative;[^}]*padding-right:\s*96rpx;') {
throw 'G-01 add sheet heading does not own the close-control positioning context.'
}
if ($page -match '(?s)\.add-dialog__heading\s*\{[^}]*top\s*:') {
throw 'G-01 add sheet heading must use real flow spacing instead of a visual offset.'
}
if ($page -notmatch '(?s)\.add-dialog__close\s*\{[^}]*position:\s*absolute;[^}]*top:\s*0;[^}]*right:\s*-22rpx;') {
throw 'G-01 add sheet close control does not follow the centered heading.'
}
if ($page -notmatch [regex]::Escape('/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png')) {
throw 'G-01 add sheet does not use the approved complete background asset.'
}
if ($page -notmatch '(?s)\.add-dialog\s*\{[^}]*border-image-source:\s*url\("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3\.png"\);[^}]*border-image-slice:\s*220\s+0\s+1\s+0\s+fill;[^}]*border-image-width:\s*118rpx\s+0\s+1rpx;') {
throw 'G-01 add sheet does not preserve the complete sheet top while stretching only the paper body.'
}
if ($page -notmatch '(?s)\.add-dialog__close-icon\s*\{[^}]*width:\s*80rpx;[^}]*height:\s*80rpx;') {
throw 'G-01 add sheet close icon canvas does not compensate for the asset transparent padding.'
}
if ($page -match '(?s)\.add-dialog\s+\.app-button\s*\{') { throw 'G-01 add sheet must not restyle the existing AppButton.' }
if ($page -notmatch '(?s)\.add-dialog__actions\s*\{[^}]*margin:\s*62rpx\s+-32rpx\s+0;') {
throw 'G-01 add sheet actions do not compensate for the existing button assets transparent side padding.'
}
if ($page -notmatch '(?s)\.add-dialog__actions\s*>\s*\.app-button\s*\{[^}]*width:\s*595rpx;[^}]*max-width:\s*100%;[^}]*min-height:\s*96rpx;') {
throw 'G-01 add sheet buttons do not keep the approved equal-width geometry.'
}
if ($page -notmatch '(?s)\.add-dialog__actions\s*\{[^}]*align-items:\s*center;') {
throw 'G-01 add sheet buttons are not centered after equal-width sizing.'
}
if ($page -notmatch '(?s)\.add-dialog__actions\s*>\s*\.app-button\s*\+\s*\.app-button\s*\{[^}]*margin-top:\s*24rpx;') {
throw 'G-01 add sheet buttons do not keep the approved 24rpx spacing.'
}
if ($page -notmatch '(?s)\.genealogy-switcher-layer\s*\{[^}]*align-items:\s*center;[^}]*padding:\s*40rpx;') {
throw 'G-01 switcher must remain a centered dialog.'
}
if ($page -match 'class="genealogy-switcher__skin"') {
throw 'G-01 switcher must not render the complete background as a fixed aspectFit image.'
}
if ($page -notmatch '(?s)\.genealogy-switcher\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*border-image-source:\s*url\("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3\.png"\);[^}]*border-image-slice:\s*300\s+260\s+360\s+260\s+fill;[^}]*border-image-width:\s*110rpx\s+48rpx\s+132rpx\s+48rpx;') {
throw 'G-01 switcher does not use the approved stretchable complete background.'
}
if ($page -notmatch '(?s)\.genealogy-switcher__content\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*padding:\s*120rpx\s+58rpx\s+140rpx;') {
throw 'G-01 switcher content does not keep the approved decoration safe area.'
}
if ($page -notmatch 'class="genealogy-switcher__list"[^>]*scroll-y') {
throw 'G-01 switcher does not provide an independent scroll list.'
}
if ($page -notmatch '(?s)class="genealogy-switcher__close"[^>]*aria-label="\u5173\u95ed".*?g01-dialog-close\.png') {
throw 'G-01 switcher does not use the custom accessible close control.'
}
if ($page -match 'class="dialog-close"[^>]*@click="closeSwitcher"') {
throw 'G-01 switcher still exposes the obsolete bottom close copy.'
}
if ($page -notmatch '(?s)onBackPress\(\(\)\s*=>\s*\{\s*if\s*\(switcherVisible\.value\)\s*\{\s*closeSwitcher\(\);\s*return\s*true;\s*\}\s*if\s*\(addDialogVisible\.value\)') {
throw 'G-01 switcher does not consume Android back before the add dialog and page navigation.'
}
if ($page -notmatch 'import\s*\{[^}]*onBackPress[^}]*\}\s*from\s*"@dcloudio/uni-app"') {
throw 'G-01 add sheet does not import onBackPress.'
}
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
if ($page -match [regex]::Escape($nativeUi)) { throw "G-01 must not use native UniApp UI: $nativeUi" }
}
@@ -78,8 +189,9 @@ foreach ($token in @('current-summary', 'current-meta-item', 'current-seal-frame
if ($page -notmatch 'width:\s*82rpx') { throw 'G-01 shortcut icons remain below the reference visual size.' }
if ($page -notmatch '<GenealogyPageBackground\s*/>') { throw 'G-01 does not consume the shared genealogy background component.' }
if ($page -notmatch 'section-divider\.png') { throw 'G-01 does not consume the approved section divider asset.' }
$pageWithoutAddMarkup = $page.Replace($addMarkup, '')
foreach ($legacyBackground in @('page-paper.jpg', 'footer-mountain-bamboo.png', 'page-paper-texture', 'page-footer-landscape')) {
if ($page -match [regex]::Escape($legacyBackground)) { throw "G-01 must replace the legacy layered background: $legacyBackground" }
if ($pageWithoutAddMarkup -match [regex]::Escape($legacyBackground)) { throw "G-01 must replace the legacy layered page background: $legacyBackground" }
}
$backgroundComponent = Get-Content -Raw -Encoding UTF8 (Join-Path $root 'components/GenealogyPageBackground.vue')
if ($backgroundComponent -notmatch 'mode="widthFix"') { throw 'G shared background must preserve the complete C artwork without horizontal cropping.' }
@@ -127,8 +239,22 @@ if ($card -match 'calendar-v1\.png') { throw 'G-01 list shows a calendar icon th
if ($card -match 'genealogy\.surname') { throw 'G-01 list seal still shows a surname instead of the fixed 家谱 seal.' }
if ($card -match 'card-copy') { throw 'G-01 list still retains the superseded split content column.' }
if ($card -notmatch '(?s)\.card-title-row\s*\{.*?justify-content:\s*space-between') { throw 'G-01 list title, role, and chevron do not share the required first row.' }
if ($card -notmatch '(?s)\.card-meta\s*\{.*?font-size:\s*26rpx') { throw 'G-01 list metadata is below the approved readable size.' }
if ($card -notmatch '(?s)\.card-updated\s*\{.*?font-size:\s*22rpx') { throw 'G-01 list update date is below the approved readable size.' }
if ($page -notmatch '(?s)\.current-switch-copy\s*\{[^}]*font-size:\s*28rpx') { throw 'G-01 switch copy does not use the approved 28rpx size.' }
if ($page -notmatch '(?s)\.current-meta\s*\{[^}]*font-size:\s*27rpx') { throw 'G-01 current genealogy metadata must retain its original 27rpx size.' }
if ($page -notmatch '(?s)\.current-meta-icon\s*\{[^}]*width:\s*36rpx;[^}]*height:\s*36rpx;[^}]*opacity:\s*1;') { throw 'G-01 current genealogy metadata icons do not use the approved 36rpx size and full opacity.' }
if ($page -notmatch '(?s)@media screen and \(max-width:\s*340px\)[^{]*\{.*?\.current-meta-icon\s*\{[^}]*width:\s*32rpx;[^}]*height:\s*32rpx;') { throw 'G-01 compact viewport metadata icons do not preserve the approved 32rpx size.' }
if ($card -notmatch '(?s)\.card-meta-icon\s*\{[^}]*width:\s*46rpx;[^}]*height:\s*46rpx;[^}]*flex:\s*0 0 auto;[^}]*opacity:\s*1;[^}]*filter:\s*saturate\(1\.35\) brightness\(0\.82\) contrast\(1\.15\);') { throw 'G-01 list location and member icons do not use the approved balanced size and contrast.' }
if ($card -notmatch '(?s)\.card-meta-item\s*\{[^}]*margin-right:\s*0;') { throw 'G-01 list metadata groups still retain the superseded trailing margin.' }
if ($card -notmatch '(?s)\.card-meta-item\s*\+\s*\.card-meta-item\s*\{[^}]*margin-left:\s*18rpx;') { throw 'G-01 list metadata groups do not keep the approved 18rpx separation.' }
if ($card -notmatch '(?s)\.genealogy-card\s*\{[^}]*min-height:\s*178rpx;') { throw 'G-01 list cards do not reserve the approved height for the update-date row.' }
if ($card -notmatch '(?s)\.card-detail-row\s*\{[^}]*flex-wrap:\s*wrap;') { throw 'G-01 list detail row does not allow the update date to occupy its own row.' }
if ($card -notmatch '(?s)<view class="card-detail-row">\s*<view class="card-updated">.*?</view>\s*<view class="card-metas">') { throw 'G-01 update date must precede metadata in the approved visual and DOM order.' }
if ($card -notmatch '(?s)\.card-updated\s*\{[^}]*width:\s*100%;[^}]*justify-content:\s*flex-end;[^}]*margin-top:\s*0;[^}]*margin-left:\s*0;') { throw 'G-01 update date does not use the approved right-aligned second row.' }
if ($card -notmatch '(?s)\.card-metas\s*\{[^}]*width:\s*100%;[^}]*margin-top:\s*2rpx;') { throw 'G-01 list metadata does not use the approved third row.' }
if ($card -notmatch '(?s)@media screen and \(max-width:\s*340px\)[^{]*\{.*?\.genealogy-card\s*\{[^}]*min-height:\s*148rpx;') { throw 'G-01 compact list cards must retain their original 148rpx height.' }
if ($card -notmatch '(?s)\.card-meta\s*\{[^}]*font-size:\s*26rpx') { throw 'G-01 list metadata must retain its original 26rpx size.' }
if ($card -notmatch '(?s)\.card-updated\s*\{[^}]*font-size:\s*24rpx') { throw 'G-01 list update date must retain its original 24rpx size.' }
if ($card -notmatch '(?s)\.card-role\s*\{[^}]*font-size:\s*26rpx') { throw 'G-01 list role must retain its original 26rpx size.' }
if ($page -notmatch '(?s)\.create-action\s*>\s*text\s*\{.*?white-space:\s*nowrap') { throw 'G-01 create action text can wrap onto multiple lines.' }
if ($page -notmatch '(?s)\.application-record\s*\+\s*\.application-record\s*\{[^}]*margin-top:\s*12rpx') { throw 'G-01 adjacent application cards must keep the approved 12rpx separation.' }
if ($page -notmatch '(?s)\.application-record__copy\s*\{[^}]*color:\s*#62584c;[^}]*font-size:\s*28rpx;[^}]*font-weight:\s*500;[^}]*line-height:\s*1\.4;') { throw 'G-01 application descriptions do not use the approved readable style.' }
+9 -8
View File
@@ -31,7 +31,8 @@ foreach ($required in @(
'join-state--form',
'join-state--success',
'join-state--error',
'g03-create-flow-panel.png',
'g01-empty-panel-frame.png',
'g-form-field-frame.png',
'a01-scroll-primary-v3.png',
'const genealogyPreview',
'query.genealogyId',
@@ -44,13 +45,16 @@ foreach ($required in @(
foreach ($nativeUi in @('uni.showToast', 'uni.showModal', 'uni.showLoading', 'uni.showActionSheet')) {
if ($g08 -match [regex]::Escape($nativeUi)) { throw "G08 must not use native UniApp UI: $nativeUi" }
}
if ($g08 -notmatch '(?s)\.join-field-error\s*\{[^}]*font-size:\s*24rpx;[^}]*line-height:\s*34rpx;') {
throw 'G08 inline validation must remain readable over the illustrated panel'
}
foreach ($required in @(
"const applicationState = ref('loading')",
'application-state--list',
'application-state--empty',
'application-state--error',
'application-status-card.png',
'list-slip-frame.png',
"'PENDING'",
"'APPROVED'",
"'REJECTED'",
@@ -70,8 +74,7 @@ foreach ($required in @(
'review-state--list',
'review-state--empty',
'review-state--error',
'application-record-card.png',
'application-status-card.png',
'list-slip-frame.png',
'a01-scroll-primary-v3.png',
'a01-scroll-secondary-v3.png',
'const reviewSamples',
@@ -91,9 +94,7 @@ foreach ($className in @('join-panel', 'join-action', 'application-card', 'revie
foreach ($entry in @('g07:', 'g08:', 'g09:')) {
if ($catalog -match [regex]::Escape($entry)) { throw "Page catalog must remove migrated generic entry: $entry" }
}
$asset = Join-Path $root 'static/assets/modules/genealogy/opaque/application-record-card.png'
if (-not (Test-Path -LiteralPath $asset)) { throw 'G08-G10 require application-record-card.png' }
$statusAsset = Join-Path $root 'static/assets/modules/genealogy/opaque/application-status-card.png'
if (-not (Test-Path -LiteralPath $statusAsset)) { throw 'G09 and application empty states require application-status-card.png' }
$statusAsset = Join-Path $root 'static/assets/modules/genealogy/transparent/list-slip-frame.png'
if (-not (Test-Path -LiteralPath $statusAsset)) { throw 'G09-G10 cards and application empty states require list-slip-frame.png' }
Write-Output 'G08-G10-APPLICATION-FLOW-CONTRACT PASS'